/* * Copyright (C) 2016 eschao <esc.chao@gmail.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.eschao.android.widget.pageflip; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Point; import android.graphics.PointF; import android.opengl.GLUtils; import android.util.Log; import android.view.animation.AccelerateInterpolator; import android.widget.Scroller; import static android.opengl.GLES20.GL_COLOR_BUFFER_BIT; import static android.opengl.GLES20.GL_DEPTH_BUFFER_BIT; import static android.opengl.GLES20.GL_DEPTH_TEST; import static android.opengl.GLES20.GL_LINEAR; import static android.opengl.GLES20.GL_TEXTURE0; import static android.opengl.GLES20.GL_TEXTURE_2D; import static android.opengl.GLES20.GL_TEXTURE_MAG_FILTER; import static android.opengl.GLES20.GL_TEXTURE_MIN_FILTER; import static android.opengl.GLES20.glActiveTexture; import static android.opengl.GLES20.glBindTexture; import static android.opengl.GLES20.glClear; import static android.opengl.GLES20.glClearColor; import static android.opengl.GLES20.glClearDepthf; import static android.opengl.GLES20.glEnable; import static android.opengl.GLES20.glGenTextures; import static android.opengl.GLES20.glTexParameterf; import static android.opengl.GLES20.glUniformMatrix4fv; import static android.opengl.GLES20.glUseProgram; import static android.opengl.GLES20.glViewport; /** * 3D Style Page Flip * * @author escchao */ public class PageFlip { final static String TAG = "PageFlip"; // default pixels of mesh vertex private final static int DEFAULT_MESH_VERTEX_PIXELS = 10; private final static int MESH_COUNT_THRESHOLD = 20; // The min page curl angle (5 degree) private final static int MIN_PAGE_CURL_ANGLE = 5; // The max page curl angle (5 degree) private final static int MAX_PAGE_CURL_ANGLE = 65; private final static int PAGE_CURL_ANGEL_DIFF = MAX_PAGE_CURL_ANGLE - MIN_PAGE_CURL_ANGLE; private final static float MIN_PAGE_CURL_RADIAN = (float)(Math.PI * MIN_PAGE_CURL_ANGLE / 180); private final static float MAX_PAGE_CURL_RADIAN= (float)(Math.PI * MAX_PAGE_CURL_ANGLE / 180); private final static float MIN_PAGE_CURL_TAN_OF_ANGLE = (float)Math.tan(MIN_PAGE_CURL_RADIAN); private final static float MAX_PAGE_CURL_TAN_OF_ANGEL = (float)Math.tan(MAX_PAGE_CURL_RADIAN); private final static float MAX_PAGE_CURL_ANGLE_RATIO = MAX_PAGE_CURL_ANGLE / 90f; private final static float MAX_TAN_OF_FORWARD_FLIP = (float)Math.tan(Math.PI / 6); private final static float MAX_TAN_OF_BACKWARD_FLIP = (float)Math.tan(Math.PI / 20); // width ratio of clicking to flip private final static float WIDTH_RATIO_OF_CLICK_TO_FLIP = 0.5f; // width ratio of triggering restore flip private final static float WIDTH_RATIO_OF_RESTORE_FLIP = 0.4f; // folder page shadow color buffer size private final static int FOLD_TOP_EDGE_SHADOW_VEX_COUNT = 22; // fold edge shadow color private final static float FOLD_EDGE_SHADOW_START_COLOR = 0.1f; private final static float FOLD_EDGE_SHADOW_START_ALPHA = 0.25f; private final static float FOLD_EDGE_SHADOW_END_COLOR = 0.3f; private final static float FOLD_EDGE_SHADOW_END_ALPHA = 0f; // fold base shadow color private final static float FOLD_BASE_SHADOW_START_COLOR = 0.05f; private final static float FOLD_BASE_SHADOW_START_ALPHA = 0.4f; private final static float FOLD_BASE_SHADOW_END_COLOR = 0.3f; private final static float FOLD_BASE_SHADOW_END_ALPHA = 0f; // first and second page private final static int FIRST_PAGE = 0; private final static int SECOND_PAGE = 1; private final static int PAGE_SIZE = 2; private final static int SINGLE_PAGE_MODE = 0; private final static int AUTO_PAGE_MODE = 1; // view size private GLViewRect mViewRect; // the pixel size for each mesh private int mPixelsOfMesh; // gradient shadow texture id private int mGradientShadowTextureID; // touch point and last touch point private PointF mTouchP; // the last touch point (could be deleted?) private PointF mLastTouchP; // the first touch point when finger down on the screen private PointF mStartTouchP; // the middle point between touch point and origin point private PointF mMiddleP; // from 2D perspective, the line will intersect Y axis and X axis that being // through middle point and perpendicular to the line which is from touch // point to origin point, The point on Y axis is mYFoldP, the mXFoldP is on // X axis. The mY{X}FoldP1 is up mY{X}FoldP, The mY{X}FoldP0 is under // mY{X}FoldP // // <----- Flip // ^ Y // | // + mYFoldP1 // / | // / | // / | // / | // / | // / | // / + mYFoldP // mTouchP / / | // . / / | // / / | // / / | // / / | // / . / + mYFoldP0 // / / /| // / / / | // / / / | //X <-----+------+------+---+ originP // mXFoldP1 mXFoldP mXFoldP0 // private PointF mYFoldP; private PointF mYFoldP0; private PointF mYFoldP1; private PointF mXFoldP; private PointF mXFoldP0; private PointF mXFoldP1; // ^ Y // mTouchP | // + | // \ | // \ | // A ( \| // X <--------+ originP // // A is angle between X axis and line from mTouchP to originP // the max curling angle between line from touchP to originP and X axis private float mMaxT2OAngleTan; // another max curling angle when finger moving causes the originP change // from (x, y) to (x, -y) which means mirror based on Y axis. private float mMaxT2DAngleTan; // the tan value of current curling angle // mKValue = (touchP.y - originP.y) / (touchP.x - originP.x) private float mKValue; // the length of line from mTouchP to originP private float mLenOfTouchOrigin; // the cylinder radius private float mR; // the perimeter ratio of semi-cylinder based on mLenOfTouchOrigin; private float mSemiPerimeterRatio; // Mesh count private int mMeshCount; // edges shadow width of back of fold page private ShadowWidth mFoldEdgesShadowWidth; // base shadow width of front of fold page private ShadowWidth mFoldBaseShadowWidth; // fold page and shadow vertexes private Vertexes mFoldFrontVertexes; private FoldBackVertexes mFoldBackVertexes; private ShadowVertexes mFoldEdgesShadow; private ShadowVertexes mFoldBaseShadow; // Shader program for openGL drawing private VertexProgram mVertexProgram; private FoldBackVertexProgram mFoldBackVertexProgram; private ShadowVertexProgram mShadowVertexProgram; // is vertical page flip private boolean mIsVertical; private PageFlipState mFlipState; // use for flip animation private Scroller mScroller; private Context mContext; // pages and page mode // in single page mode, there is only one page in the index 0 // in double pages mode, there are two pages, the first one is always active // page which is receiving finger events, for example: finger down/move/up private Page mPages[]; private int mPageMode; // is clicking to flip page private boolean mIsClickToFlip; // width ration of clicking to flip private float mWidthRationOfClickToFlip; // listener for page flipping private OnPageFlipListener mListener; /** * Constructor */ public PageFlip(Context context) { mContext = context; mScroller = new Scroller(context); mFlipState = PageFlipState.END_FLIP; mIsVertical = false; mViewRect = new GLViewRect(); mPixelsOfMesh = DEFAULT_MESH_VERTEX_PIXELS; mSemiPerimeterRatio = 0.8f; mIsClickToFlip = true; mListener = null; mWidthRationOfClickToFlip = WIDTH_RATIO_OF_CLICK_TO_FLIP; // init pages mPages = new Page[PAGE_SIZE]; mPageMode = SINGLE_PAGE_MODE; // key points mMiddleP = new PointF(); mYFoldP = new PointF(); mYFoldP0 = new PointF(); mYFoldP1 = new PointF(); mXFoldP = new PointF(); mXFoldP0 = new PointF(); mXFoldP1 = new PointF(); mTouchP = new PointF(); mLastTouchP = new PointF(); mStartTouchP = new PointF(); // init shadow width mFoldEdgesShadowWidth = new ShadowWidth(5, 30, 0.25f); mFoldBaseShadowWidth = new ShadowWidth(2, 40, 0.4f); // init shader program mVertexProgram = new VertexProgram(); mFoldBackVertexProgram = new FoldBackVertexProgram(); mShadowVertexProgram = new ShadowVertexProgram(); // init vertexes mFoldFrontVertexes = new Vertexes(); mFoldBackVertexes = new FoldBackVertexes(); mFoldEdgesShadow = new ShadowVertexes(FOLD_TOP_EDGE_SHADOW_VEX_COUNT, FOLD_EDGE_SHADOW_START_COLOR, FOLD_EDGE_SHADOW_START_ALPHA, FOLD_EDGE_SHADOW_END_COLOR, FOLD_EDGE_SHADOW_END_ALPHA); mFoldBaseShadow = new ShadowVertexes(0, FOLD_BASE_SHADOW_START_COLOR, FOLD_BASE_SHADOW_START_ALPHA, FOLD_BASE_SHADOW_END_COLOR, FOLD_BASE_SHADOW_END_ALPHA); } /** * Enable/disable auto page mode * <p> * The default value is single page mode, which means there is only one page * no matter what the screen is portrait or landscape. If set mode with auto * page, it will automatically detect screen mode and choose single or * double pages to render the whole screen. * </p> * * @param isAuto true if set mode with auto page * @return true if pages are recreated and need to render page */ public boolean enableAutoPage(boolean isAuto) { int newMode = isAuto ? AUTO_PAGE_MODE : SINGLE_PAGE_MODE; if (mPageMode != newMode) { mPageMode = newMode; // check if we need to re-create pages if ((newMode == AUTO_PAGE_MODE && mViewRect.surfaceW > mViewRect.surfaceH && mPages[SECOND_PAGE] == null) || (newMode == SINGLE_PAGE_MODE && mPages[SECOND_PAGE] != null)) { createPages(); return true; } } return false; } /** * Is auto page mode enabled? * * @return true if auto page mode is enabled */ public boolean isAutoPageEnabled() { return mPageMode == AUTO_PAGE_MODE; } /** * Enable/disable clicking to flip page * <p> * By default, the page flipping will only be triggered by finger. * Through this function to enable clicking, you can start flipping * page with finger click. * </p> * * @param enable true if enable it * @return self */ public PageFlip enableClickToFlip(boolean enable) { mIsClickToFlip = enable; return this; } /** * Set width ratio of clicking to flip, the default is 0.5f * <p>Which area the finger is clicking on will trigger a flip forward or * backward</p> * * @param ratio width ratio of clicking to flip, is (0 ... 0.5] * @return self */ public PageFlip setWidthRatioOfClickToFlip(float ratio) { if (ratio <= 0 || ratio > 0.5f) { throw new IllegalArgumentException("Invalid ratio value: " + ratio); } mWidthRationOfClickToFlip = ratio; return this; } /** * Set listener for page flip * <p> * Set a page flip listener to determine if page can flip forward or * backward * </p> * * @param listener a listener for page flip * @return self */ public PageFlip setListener(OnPageFlipListener listener) { mListener = listener; return this; } /** * Sets pixels of each mesh * <p>The default value is 10 pixels for each mesh</p> * * @param pixelsOfMesh pixel amount of each mesh * @return self */ public PageFlip setPixelsOfMesh(int pixelsOfMesh) { mPixelsOfMesh = pixelsOfMesh > 0 ? pixelsOfMesh : DEFAULT_MESH_VERTEX_PIXELS; return this; } /** * Get pixels of mesh vertex * * @return pixels of each mesh:w */ public int getPixelsOfMesh() { return mPixelsOfMesh; } /** * Set ratio of semi-perimeter of fold cylinder * <p> * When finger is clicking and moving on page, the page from touch point to * original point will be curled like as a cylinder, the radius of cylinder * is determined by line length from touch point to original point. You can * give a ratio of this line length to set cylinder radius, the default * value is 0.8 * </p> * * @param ratio ratio of line length from touch point to original point. Its * value is (0..1] * @return self */ public PageFlip setSemiPerimeterRatio(float ratio) { if (ratio <= 0 || ratio > 1) { throw new IllegalArgumentException("Invalid ratio value: " + ratio); } mSemiPerimeterRatio = ratio; return this; } /** * Set mask alpha for back of fold page * <p>Mask alpha will be invalid in double pages</p> * * @param alpha alpha value is in [0..255] * @return self */ public PageFlip setMaskAlphaOfFold(int alpha) { mFoldBackVertexes.setMaskAlpha(alpha); return this; } /** * Sets edge shadow color of fold page * * @param startColor shadow start color: [0..1] * @param startAlpha shadow start alpha: [0..1] * @param endColor shadow end color: [0..1] * @param endAlpha shadow end alpha: [0..1] * @return self */ public PageFlip setShadowColorOfFoldEdges(float startColor, float startAlpha, float endColor, float endAlpha) { mFoldEdgesShadow.mColor.set(startColor, startAlpha, endColor, endAlpha); return this; } /** * Sets base shadow color of fold page * * @param startColor shadow start color: [0..1] * @param startAlpha shadow start alpha: [0..1] * @param endColor shadow end color: [0..1] * @param endAlpha shadow end alpha: [0..1] * @return self */ public PageFlip setShadowColorOfFoldBase(float startColor, float startAlpha, float endColor, float endAlpha) { mFoldBaseShadow.mColor.set(startColor, startAlpha, endColor, endAlpha); return this; } /** * Set shadow width of fold edges * * @param min minimal width * @param max maximum width * @param ratio width ratio based on fold cylinder radius. It is in (0..1) * @return self */ public PageFlip setShadowWidthOfFoldEdges(float min, float max, float ratio) { mFoldEdgesShadowWidth.set(min, max, ratio); return this; } /** * Set shadow width of fold base * * @param min minimal width * @param max maximum width * @param ratio width ratio based on fold cylinder radius. It is in (0..1) * @return self */ public PageFlip setShadowWidthOfFoldBase(float min, float max, float ratio) { mFoldBaseShadowWidth.set(min, max, ratio); return this; } /** * Get surface width * * @return surface width */ public int getSurfaceWidth() { return (int)mViewRect.surfaceW; } /** * Get surface height * * @return surface height */ public int getSurfaceHeight() { return (int)mViewRect.surfaceH; } /** * Get page flip state * * @return page flip state */ public PageFlipState getFlipState() { return mFlipState; } /** * Handle surface creation event * * @throws PageFlipException if failed to compile and link OpenGL shader */ public void onSurfaceCreated() throws PageFlipException { glClearColor(0, 0, 0, 1f); glClearDepthf(1.0f); glEnable(GL_DEPTH_TEST); try { // init shader programs mVertexProgram.init(mContext); mFoldBackVertexProgram.init(mContext); mShadowVertexProgram.init(mContext); // create gradient shadow texture createGradientShadowTexture(); } catch (PageFlipException e) { mVertexProgram.delete(); mFoldBackVertexProgram.delete(); mShadowVertexProgram.delete(); throw e; } } /** * Handle surface changing event * * @param width surface width * @param height surface height * @throws PageFlipException if failed to compile and link OpenGL shader */ public void onSurfaceChanged(int width, int height) throws PageFlipException { mViewRect.set(width, height); glViewport(0, 0, width, height); mVertexProgram.initMatrix(-mViewRect.halfW, mViewRect.halfW, -mViewRect.halfH, mViewRect.halfH); computeMaxMeshCount(); createPages(); } /** * Create pages */ private void createPages() { // release textures hold in pages if (mPages[FIRST_PAGE] != null) { mPages[FIRST_PAGE].deleteAllTextures(); } if (mPages[SECOND_PAGE] != null) { mPages[SECOND_PAGE].deleteAllTextures(); } // landscape if (mPageMode == AUTO_PAGE_MODE && mViewRect.surfaceW > mViewRect.surfaceH) { mPages[FIRST_PAGE] = new Page(mViewRect.left, 0, mViewRect.top, mViewRect.bottom); mPages[SECOND_PAGE] = new Page(0, mViewRect.right, mViewRect.top, mViewRect.bottom); } else { mPages[FIRST_PAGE] = new Page(mViewRect.left, mViewRect.right, mViewRect.top, mViewRect.bottom); mPages[SECOND_PAGE] = null; } } /** * Handle finger down event * * @param touchX x of finger down point * @param touchY y of finger down point */ public void onFingerDown(float touchX, float touchY) { // covert to OpenGL coordinate touchX = mViewRect.toOpenGLX(touchX); touchY = mViewRect.toOpenGLY(touchY); // check if touch point is contained in page? boolean isContained = false; if (mPages[FIRST_PAGE].contains(touchX, touchY)) { isContained = true; } else if (mPages[SECOND_PAGE] != null && mPages[SECOND_PAGE].contains(touchX, touchY)) { // in double pages, the first page is always active page which touch // event is happening on isContained = true; Page p = mPages[SECOND_PAGE]; mPages[SECOND_PAGE] = mPages[FIRST_PAGE]; mPages[FIRST_PAGE] = p; } // point is contained, ready to flip if (isContained) { mMaxT2OAngleTan = 0f; mMaxT2DAngleTan = 0f; mLastTouchP.set(touchX, touchY); mStartTouchP.set(touchX, touchY); mTouchP.set(touchX, touchY); mFlipState = PageFlipState.BEGIN_FLIP; } } /** * Handle finger moving event * * @param touchX x of finger moving point * @param touchY y of finger moving point * @return true if moving will trigger to draw a new frame for page flip, * False means the movement should be ignored. */ public boolean onFingerMove(float touchX, float touchY) { touchX = mViewRect.toOpenGLX(touchX); touchY = mViewRect.toOpenGLY(touchY); // compute moving distance (dx, dy) float dy = (touchY - mStartTouchP.y); float dx = (touchX - mStartTouchP.x); final Page page = mPages[FIRST_PAGE]; final GLPoint originP = page.originP; final GLPoint diagonalP = page.diagonalP; // begin to move if (mFlipState == PageFlipState.BEGIN_FLIP && (Math.abs(dx) > mViewRect.width * 0.05f)) { // set OriginP and DiagonalP points page.setOriginAndDiagonalPoints(mPages[SECOND_PAGE] != null, dy); // compute max degree between X axis and line from TouchP to OriginP // and max degree between X axis and line from TouchP to // (OriginP.x, DiagonalP.Y) float y2o = Math.abs(mStartTouchP.y - originP.y); float y2d = Math.abs(mStartTouchP.y - diagonalP.y); mMaxT2OAngleTan = computeTanOfCurlAngle(y2o); mMaxT2DAngleTan = computeTanOfCurlAngle(y2d); // moving at the top and bottom screen have different tan value of // angle if ((originP.y < 0 && page.right > 0) || (originP.y > 0 && page.right <= 0)) { mMaxT2OAngleTan = -mMaxT2OAngleTan; } else { mMaxT2DAngleTan = -mMaxT2DAngleTan; } // determine if it is moving backward or forward if (mPages[SECOND_PAGE] == null && dx > 0 && mListener != null && mListener.canFlipBackward()) { mStartTouchP.x = originP.x; dx = (touchX - mStartTouchP.x); mFlipState = PageFlipState.BACKWARD_FLIP; } else if (mListener != null && mListener.canFlipForward() && (dx < 0 && originP.x > 0 || dx > 0 && originP.x < 0)) { mFlipState = PageFlipState.FORWARD_FLIP; } } // in moving, compute the TouchXY if (mFlipState == PageFlipState.FORWARD_FLIP || mFlipState == PageFlipState.BACKWARD_FLIP || mFlipState == PageFlipState.RESTORE_FLIP) { // check if page is flipping vertically mIsVertical = Math.abs(dy) <= 1f; // multiply a factor to make sure the touch point is always head of // finger point if (PageFlipState.FORWARD_FLIP == mFlipState) { dx *= 1.2f; } else { dx *= 1.1f; } // moving direction is changed: // 1. invert max curling angle // 2. invert Y of original point and diagonal point if ((dy < 0 && originP.y < 0) || (dy > 0 && originP.y > 0)) { float t = mMaxT2DAngleTan; mMaxT2DAngleTan = mMaxT2OAngleTan; mMaxT2OAngleTan = t; page.invertYOfOriginPoint(); } // compute new TouchP.y float maxY = dx * mMaxT2OAngleTan; if (Math.abs(dy) > Math.abs(maxY)) { dy = maxY; } // check if XFoldX1 is outside page width, if yes, recompute new // TouchP.y to assure the XFoldX1 is in page width float t2oK = dy / dx; float xTouchX = dx + dy * t2oK; float xRatio = (1 + mSemiPerimeterRatio) * 0.5f; float xFoldX1 = xRatio * xTouchX; if (Math.abs(xFoldX1) + 2 >= page.width) { float dy2 = ((diagonalP.x - originP.x) / xRatio - dx) * dx; // ignore current moving if we can't get a valid dy, for example // , in double pages mode, when finger is moving from the one // page to another page, the dy2 is negative and should be // ignored if (dy2 < 0) { return false; } double t = Math.sqrt(dy2); if (originP.y > 0) { t = -t; dy = (int)Math.ceil(t); } else { dy = (int)Math.floor(t); } } // set touchP(x, y) and middleP(x, y) mLastTouchP.set(touchX, touchY); mTouchP.set(dx + originP.x, dy + originP.y); mMiddleP.x = (mTouchP.x + originP.x) * 0.5f; mMiddleP.y = (mTouchP.y + originP.y) * 0.5f; // continue to compute points to drawing flip computeVertexesAndBuildPage(); return true; } return false; } /** * Handle finger up event * * @param touchX x of finger moving point * @param touchY y of finger moving point * @param duration millisecond for page flip animation * @return true if animation is started or animation is not triggered */ public boolean onFingerUp(float touchX, float touchY, int duration) { touchX = mViewRect.toOpenGLX(touchX); touchY = mViewRect.toOpenGLY(touchY); final Page page = mPages[FIRST_PAGE]; final GLPoint originP = page.originP; final GLPoint diagonalP = page.diagonalP; final boolean hasSecondPage = mPages[SECOND_PAGE] != null; Point start = new Point((int)mTouchP.x, (int)mTouchP.y); Point end = new Point(0, 0); // forward flipping if (mFlipState == PageFlipState.FORWARD_FLIP) { // can't going forward, restore current page if (page.isXInRange(touchX, WIDTH_RATIO_OF_RESTORE_FLIP)) { end.x = (int)originP.x; mFlipState = PageFlipState.RESTORE_FLIP; } else if (hasSecondPage && originP.x < 0) { end.x = (int)(diagonalP.x + page.width); } else { end.x = (int)(diagonalP.x - page.width); } end.y = (int)(originP.y); } // backward flipping else if (mFlipState == PageFlipState.BACKWARD_FLIP) { // if not over middle x, change from backward to forward to restore if (!page.isXInRange(touchX, 0.5f)) { mFlipState = PageFlipState.FORWARD_FLIP; end.set((int)(diagonalP.x - page.width), (int)originP.y); } else { mMaxT2OAngleTan = (mTouchP.y - originP.y) / (mTouchP.x - originP.x); end.set((int) originP.x, (int) originP.y); } } // ready to flip else if (mFlipState == PageFlipState.BEGIN_FLIP) { mIsVertical = false; mFlipState = PageFlipState.END_FLIP; page.setOriginAndDiagonalPoints(hasSecondPage, -touchY); // if enable clicking to flip, compute scroller points for animation if (mIsClickToFlip && Math.abs(touchX - mStartTouchP.x) < 2) { computeScrollPointsForClickingFlip(touchX, start, end); } } // start scroller for animating if (mFlipState == PageFlipState.FORWARD_FLIP || mFlipState == PageFlipState.BACKWARD_FLIP || mFlipState == PageFlipState.RESTORE_FLIP) { mScroller.startScroll(start.x, start.y, end.x - start.x, end.y - start.y, duration); return true; } return false; } /** * Check finger point to see if it can trigger a flip animation * * @param touchX x of finger point * @param touchY y of finger point * @return true if the point can trigger a flip animation */ public boolean canAnimate(float touchX, float touchY) { return (mFlipState == PageFlipState.FORWARD_FLIP && !mPages[FIRST_PAGE].contains(mViewRect.toOpenGLX(touchX), mViewRect.toOpenGLY(touchY))); } /** * Compute scroller points for animating * * @param x x of clicking point * @param start start point of scroller will be set * @param end end point of scroller will be set */ private void computeScrollPointsForClickingFlip(float x, Point start, Point end) { Page page = mPages[FIRST_PAGE]; GLPoint originP = page.originP; GLPoint diagonalP = page.diagonalP; final boolean hasSecondPage = mPages[SECOND_PAGE] != null; // forward and backward flip have different degree float tanOfForwardAngle = MAX_TAN_OF_FORWARD_FLIP; float tanOfBackwardAngle = MAX_TAN_OF_BACKWARD_FLIP; if ((originP.y < 0 && originP.x > 0) || (originP.y > 0 && originP.x < 0)) { tanOfForwardAngle = -tanOfForwardAngle; tanOfBackwardAngle = -tanOfBackwardAngle; } // backward flip if (!hasSecondPage && x < diagonalP.x + page.width * mWidthRationOfClickToFlip && mListener != null && mListener.canFlipBackward()) { mFlipState = PageFlipState.BACKWARD_FLIP; mKValue = tanOfBackwardAngle; start.set((int)diagonalP.x, (int)(originP.y + (start.x - originP.x) * mKValue)); end.set((int)originP.x - 5, (int)originP.y); } // forward flip else if (mListener != null && mListener.canFlipForward() && page.isXInRange(x, mWidthRationOfClickToFlip)) { mFlipState = PageFlipState.FORWARD_FLIP; mKValue = tanOfForwardAngle; // compute start.x if (originP.x < 0) { start.x = (int)(originP.x + page.width * 0.25f); } else { start.x = (int)(originP.x - page.width * 0.25f); } // compute start.y start.y = (int)(originP.y + (start.x - originP.x) * mKValue); // compute end.x // left page in double page mode if (hasSecondPage && originP.x < 0) { end.x = (int)(diagonalP.x + page.width); } // right page in double page mode else { end.x = (int)(diagonalP.x - page.width); } end.y = (int)(originP.y); } } /** * Compute animating and check if it can continue * * @return true animating is continue or it is stopped */ public boolean animating() { final Page page = mPages[FIRST_PAGE]; final GLPoint originP = page.originP; final GLPoint diagonalP = page.diagonalP; // is to end animating? boolean isAnimating = !mScroller.isFinished(); if (isAnimating) { // get new (x, y) mScroller.computeScrollOffset(); mTouchP.set(mScroller.getCurrX(), mScroller.getCurrY()); // for backward and restore flip, compute x to check if it can // continue to flip if (mFlipState == PageFlipState.BACKWARD_FLIP || mFlipState == PageFlipState.RESTORE_FLIP) { mTouchP.y = (mTouchP.x - originP.x) * mKValue + originP.y; isAnimating = Math.abs(mTouchP.x - originP.x) > 10; } // check if flip is vertical else { mIsVertical = Math.abs(mTouchP.y - originP.y) < 1f; } // compute middle point mMiddleP.set((mTouchP.x + originP.x) * 0.5f, (mTouchP.y + originP.y) * 0.5f); // compute key points if (mIsVertical) { computeKeyVertexesWhenVertical(); } else { computeKeyVertexesWhenSlope(); } // in double page mode if (mPages[SECOND_PAGE] != null) { // if the xFoldP1.x is outside page width, need to limit // xFoldP1.x is in page.width and recompute new key points so // that the page flip is still going forward if (page.isXOutsidePage(mXFoldP1.x)) { mXFoldP1.x = diagonalP.x; float cosA = (mTouchP.x - originP.x) / mLenOfTouchOrigin; float ratio = 1 - page.width * Math.abs(cosA) / mLenOfTouchOrigin; mR = (float)(mLenOfTouchOrigin * (1 - 2 * ratio) / Math.PI); mXFoldP0.x = mLenOfTouchOrigin * ratio / cosA + originP.x; if (mIsVertical) { mYFoldP0.x = mXFoldP0.x; mYFoldP1.x = mXFoldP1.x; } else { mYFoldP1.y = originP.y + (mXFoldP1.x - originP.x) / mKValue; mYFoldP0.y = originP.y + (mXFoldP0.x - originP.x) / mKValue; } // re-compute mesh count float len = Math.abs(mMiddleP.x - mXFoldP0.x); if (mMeshCount > len) { mMeshCount = (int)len; } isAnimating = mMeshCount > 0 && Math.abs(mXFoldP0.x - diagonalP.x) >= 2; } } // in single page mode, check if the whole fold page is outside the // screen and animating should be stopped else if (mFlipState == PageFlipState.FORWARD_FLIP) { float r = (float)(mLenOfTouchOrigin * mSemiPerimeterRatio / Math.PI); float x = (mYFoldP1.y - diagonalP.y) * mKValue + r; isAnimating = x > (diagonalP.x - originP.x); } } // animation is stopped if (!isAnimating) { abortAnimating(); } // continue animation and compute vertexes else if (mIsVertical) { computeVertexesWhenVertical(); } else { computeVertexesWhenSlope(); } return isAnimating; } /** * Is animating ? * * @return true if page is flipping */ public boolean isAnimating() { return !mScroller.isFinished(); } /** * Abort animating */ public void abortAnimating() { mScroller.abortAnimation(); if (mFlipState == PageFlipState.FORWARD_FLIP) { mFlipState = PageFlipState.END_WITH_FORWARD; } else if (mFlipState == PageFlipState.BACKWARD_FLIP) { mFlipState = PageFlipState.END_WITH_BACKWARD; } else if (mFlipState == PageFlipState.RESTORE_FLIP) { mFlipState = PageFlipState.END_WITH_RESTORE; } } /** * Is animation stated? * * @return true if flip is started */ public boolean isStartedFlip() { return mFlipState == PageFlipState.BACKWARD_FLIP || mFlipState == PageFlipState.FORWARD_FLIP || mFlipState == PageFlipState.RESTORE_FLIP; } /** * The moving is ended? * * @return true if flip is ended */ public boolean isEndedFlip() { return mFlipState == PageFlipState.END_FLIP || mFlipState == PageFlipState.END_WITH_RESTORE || mFlipState == PageFlipState.END_WITH_BACKWARD || mFlipState == PageFlipState.END_WITH_FORWARD; } /** * Get the first page * First page is currently operating page which means it is the page user * finger is clicking or moving * * @return flip state, See {@link PageFlipState} */ public Page getFirstPage() { return mPages[FIRST_PAGE]; } /** * Get the second page * <p> * Second page is only valid in double page mode, if it is null, that means * there is only one page for whole screen whatever the screen is portrait * or landscape * </p> * * @return the second page, null if no second page */ public Page getSecondPage() { return mPages[SECOND_PAGE]; } /** * Delete unused textures */ public void deleteUnusedTextures() { mPages[FIRST_PAGE].deleteUnusedTextures(); if (mPages[SECOND_PAGE] != null) { mPages[SECOND_PAGE].deleteUnusedTextures(); } } /** * Draw flipping frame */ public void drawFlipFrame() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); final boolean hasSecondPage = mPages[SECOND_PAGE] != null; // 1. draw back of fold page glUseProgram(mFoldBackVertexProgram.mProgramRef); glActiveTexture(GL_TEXTURE0); mFoldBackVertexes.draw(mFoldBackVertexProgram, mPages[FIRST_PAGE], hasSecondPage, mGradientShadowTextureID); // 2. draw unfold page and front of fold page glUseProgram(mVertexProgram.mProgramRef); glActiveTexture(GL_TEXTURE0); mPages[FIRST_PAGE].drawFrontPage(mVertexProgram, mFoldFrontVertexes); if (hasSecondPage) { mPages[SECOND_PAGE].drawFullPage(mVertexProgram, true); } // 3. draw edge and base shadow of fold parts glUseProgram(mShadowVertexProgram.mProgramRef); mFoldBaseShadow.draw(mShadowVertexProgram); mFoldEdgesShadow.draw(mShadowVertexProgram); } /** * Draw frame with full page */ public void drawPageFrame() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glUseProgram(mVertexProgram.mProgramRef); glUniformMatrix4fv(mVertexProgram.mMVPMatrixLoc, 1, false, VertexProgram.MVPMatrix, 0); glActiveTexture(GL_TEXTURE0); // 1. draw first page mPages[FIRST_PAGE].drawFullPage(mVertexProgram, true); // 2. draw second page if have if (mPages[SECOND_PAGE] != null) { mPages[SECOND_PAGE].drawFullPage(mVertexProgram, true); } } /** * Compute max mesh count and allocate vertexes buffer */ private void computeMaxMeshCount() { // compute max mesh count int maxMeshCount = (int)mViewRect.minOfWH() / mPixelsOfMesh; // make sure the vertex count is even number if (maxMeshCount % 2 != 0) { maxMeshCount++; } // init vertexes buffers mFoldBackVertexes.set(maxMeshCount + 2); mFoldFrontVertexes.set((maxMeshCount << 1) + 8, 3, true); mFoldEdgesShadow.set(maxMeshCount + 2); mFoldBaseShadow.set(maxMeshCount + 2); } /** * Create gradient shadow texture for lighting effect */ private void createGradientShadowTexture() { int textureIDs[] = new int[1]; glGenTextures(1, textureIDs, 0); glActiveTexture(GL_TEXTURE0); mGradientShadowTextureID = textureIDs[0]; // gradient shadow texture Bitmap shadow = PageFlipUtils.createGradientBitmap(); glBindTexture(GL_TEXTURE_2D, mGradientShadowTextureID); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); GLUtils.texImage2D(GL_TEXTURE_2D, 0, shadow, 0); shadow.recycle(); } /** * Compute vertexes of page */ private void computeVertexesAndBuildPage() { if (mIsVertical) { computeKeyVertexesWhenVertical(); computeVertexesWhenVertical(); } else { computeKeyVertexesWhenSlope(); computeVertexesWhenSlope(); } } /** * Compute key vertexes when page flip is vertical */ private void computeKeyVertexesWhenVertical() { final float oX = mPages[FIRST_PAGE].originP.x ; final float oY = mPages[FIRST_PAGE].originP.y; final float dY = mPages[FIRST_PAGE].diagonalP.y; mTouchP.y = oY; mMiddleP.y = oY; // set key point on X axis float r0 = 1 - mSemiPerimeterRatio; float r1 = 1 + mSemiPerimeterRatio; mXFoldP.set(mMiddleP.x, oY); mXFoldP0.set(oX + (mXFoldP.x - oX) * r0, mXFoldP.y); mXFoldP1.set(oX + r1 * (mXFoldP.x - oX), mXFoldP.y); // set key point on Y axis mYFoldP.set(mMiddleP.x, dY); mYFoldP0.set(mXFoldP0.x, mYFoldP.y); mYFoldP1.set(mXFoldP1.x, mYFoldP.y); // line length from mTouchP to originP mLenOfTouchOrigin = Math.abs(mTouchP.x - oX); mR = (float)(mLenOfTouchOrigin * mSemiPerimeterRatio / Math.PI); // compute mesh count computeMeshCount(); } /** * Compute all vertexes when page flip is vertical */ private void computeVertexesWhenVertical() { float x = mMiddleP.x; float stepX = (mMiddleP.x - mXFoldP0.x) / mMeshCount; final Page page = mPages[FIRST_PAGE]; final float oY = page.originP.y; final float dY = page.diagonalP.y; final float cDY = page.diagonalP.texY; final float cOY = page.originP.texY; final float cOX = page.originP.texX; // compute the point on back page half cylinder mFoldBackVertexes.reset(); for (int i = 0; i <= mMeshCount; ++i, x -= stepX) { // compute radian of x point float x2t = x - mXFoldP1.x; float radius = x2t / mR; float sinR = (float)Math.sin(radius); float coordX = page.textureX(x); float fx = mXFoldP1.x + mR * sinR; float fz = (float) (mR * (1 - Math.cos(radius))); // compute vertex when it is curled mFoldBackVertexes.addVertex(fx, dY, fz, sinR, coordX, cDY) .addVertex(fx, oY, fz, sinR, coordX, cOY); } float tx0 = mTouchP.x; mFoldBackVertexes.addVertex(tx0, dY, 1, 0, cOX, cDY) .addVertex(tx0, oY, 1, 0, cOX, cOY) .toFloatBuffer(); // compute shadow width float sw = -mFoldEdgesShadowWidth.width(mR); float bw = mFoldBaseShadowWidth.width(mR); if (page.originP.x < 0) { sw = -sw; bw = -bw; } // fold base shadow float bx0 = mFoldBackVertexes.mVertexes[0]; mFoldBaseShadow.setVertexes(0, bx0, oY, bx0 + bw, oY) .setVertexes(8, bx0, dY, bx0 + bw, dY) .toFloatBuffer(16); // fold edge shadow mFoldEdgesShadow.setVertexes(0, tx0, oY, tx0 + sw, oY) .setVertexes(8, tx0, dY, tx0 + sw, dY) .toFloatBuffer(16); // fold front mFoldFrontVertexes.reset(); page.buildVertexesOfPageWhenVertical(mFoldFrontVertexes, mXFoldP1); mFoldFrontVertexes.toFloatBuffer(); } /** * Compute key vertexes when page flip is slope */ private void computeKeyVertexesWhenSlope() { final float oX = mPages[FIRST_PAGE].originP.x; final float oY = mPages[FIRST_PAGE].originP.y; float dX = mMiddleP.x - oX; float dY = mMiddleP.y - oY; // compute key points on X axis float r0 = 1 - mSemiPerimeterRatio; float r1 = 1 + mSemiPerimeterRatio; mXFoldP.set(mMiddleP.x + dY * dY / dX, oY); mXFoldP0.set(oX + (mXFoldP.x - oX) * r0, mXFoldP.y); mXFoldP1.set(oX + r1 * (mXFoldP.x - oX), mXFoldP.y); // compute key points on Y axis mYFoldP.set(oX, mMiddleP.y + dX * dX / dY); mYFoldP0.set(mYFoldP.x, oY + (mYFoldP.y - oY) * r0); mYFoldP1.set(mYFoldP.x, oY + r1 * (mYFoldP.y - oY)); // line length from TouchXY to OriginalXY mLenOfTouchOrigin = (float)Math.hypot((mTouchP.x - oX), (mTouchP.y - oY)); // cylinder radius mR = (float)(mLenOfTouchOrigin * mSemiPerimeterRatio / Math.PI); // compute line slope mKValue = (mTouchP.y - oY) / (mTouchP.x - oX); // compute mesh count computeMeshCount(); } /** * Compute back vertex and edge shadow vertex of fold page * <p> * In 2D coordinate system, for every vertex on fold page, we will follow * the below steps to compute its 3D point (x,y,z) on curled page(cylinder): * </p> * <ul> * <li>deem originP as (0, 0) to simplify the next computing steps</li> * <li>translate point(x, y) to new coordinate system * (originP is (0, 0))</li> * <li>rotate point(x, y) with curling angle A in clockwise</li> * <li>compute 3d point (x, y, z) for 2d point(x, y), at this time, the * cylinder is vertical in new coordinate system which will help us * compute point</li> * <li>rotate 3d point (x, y, z) with -A to restore</li> * <li>translate 3d point (x, y, z) to original coordinate system</li> * </ul> * * <p>For point of edge shadow, the most computing steps are same but:</p> * <ul> * <li>shadow point is following the page point except different x * coordinate</li> * <li>shadow point has same z coordinate with the page point</li> * </ul> * * @param isX is vertex for x point on x axis or y point on y axis? * @param x0 x of point on axis * @param y0 y of point on axis * @param sx0 x of edge shadow point * @param sy0 y of edge shadow point * @param tX x of xFoldP1 point in rotated coordinate system * @param sinA sin value of page curling angle * @param cosA cos value of page curling angel * @param coordX x of texture coordinate * @param coordY y of texture coordinate * @param oX x of originate point * @param oY y of originate point */ private void computeBackVertex(boolean isX, float x0, float y0, float sx0, float sy0, float tX, float sinA, float cosA, float coordX, float coordY, float oX, float oY) { // rotate degree A float x = x0 * cosA - y0 * sinA; float y = x0 * sinA + y0 * cosA; // rotate degree A for vertexes of fold edge shadow float sx = sx0 * cosA - sy0 * sinA; float sy = sx0 * sinA + sy0 * cosA; // compute mapping point on cylinder float rad = (x - tX) / mR; double sinR = Math.sin(rad); x = (float) (tX + mR * sinR); float cz = (float) (mR * (1 - Math.cos(rad))); // rotate degree -A, sin(-A) = -sin(A), cos(-A) = cos(A) float cx = x * cosA + y * sinA + oX; float cy = y * cosA - x * sinA + oY; mFoldBackVertexes.addVertex(cx, cy, cz, (float)sinR, coordX, coordY); // compute coordinates of fold shadow edge float sRadian = (sx - tX) / mR; sx = (float)(tX + mR * Math.sin(sRadian)); mFoldEdgesShadow.addVertexes(isX, cx, cy, sx * cosA + sy * sinA + oX, sy * cosA - sx * sinA + oY); } /** * Compute back vertex of fold page * <p> * Almost same with another computeBackVertex function except expunging the * shadow point part * </p> * * @param x0 x of point on axis * @param y0 y of point on axis * @param tX x of xFoldP1 point in rotated coordinate system * @param sinA sin value of page curling angle * @param cosA cos value of page curling angel * @param coordX x of texture coordinate * @param coordY y of texture coordinate * @param oX x of originate point * @param oY y of originate point */ private void computeBackVertex(float x0, float y0, float tX, float sinA, float cosA, float coordX, float coordY, float oX, float oY) { // rotate degree A float x = x0 * cosA - y0 * sinA; float y = x0 * sinA + y0 * cosA; // compute mapping point on cylinder float rad = (x - tX) / mR; double sinR = Math.sin(rad); x = (float) (tX + mR * sinR); float cz = (float) (mR * (1 - Math.cos(rad))); // rotate degree -A, sin(-A) = -sin(A), cos(-A) = cos(A) float cx = x * cosA + y * sinA + oX; float cy = y * cosA - x * sinA + oY; mFoldBackVertexes.addVertex(cx, cy, cz, (float)sinR, coordX, coordY); } /** * Compute front vertex and base shadow vertex of fold page * <p>The computing principle is almost same with * {@link #computeBackVertex(boolean, float, float, float, float, float, * float, float, float, float, float, float)}</p> * * @param isX is vertex for x point on x axis or y point on y axis? * @param x0 x of point on axis * @param y0 y of point on axis * @param tX x of xFoldP1 point in rotated coordinate system * @param sinA sin value of page curling angle * @param cosA cos value of page curling angel * @param baseWcosA base shadow width * cosA * @param baseWsinA base shadow width * sinA * @param coordX x of texture coordinate * @param coordY y of texture coordinate * @param oX x of originate point * @param oY y of originate point */ private void computeFrontVertex(boolean isX, float x0, float y0, float tX, float sinA, float cosA, float baseWcosA, float baseWsinA, float coordX, float coordY, float oX, float oY, float dY) { // rotate degree A float x = x0 * cosA - y0 * sinA; float y = x0 * sinA + y0 * cosA; // compute mapping point on cylinder float rad = (x - tX)/ mR; x = (float)(tX + mR * Math.sin(rad)); float cz = (float)(mR * (1 - Math.cos(rad))); // rotate degree -A, sin(-A) = -sin(A), cos(-A) = cos(A) float cx = x * cosA + y * sinA + oX; float cy = y * cosA - x * sinA + oY; mFoldFrontVertexes.addVertex(cx, cy, cz, coordX, coordY); mFoldBaseShadow.addVertexes(isX, cx, cy, cx + baseWcosA, cy - baseWsinA); } /** * Compute front vertex * <p>The difference with another * {@link #computeFrontVertex(boolean, float, float, float, float, float, * float, float, float, float, float, float, float)} is that it won't * compute base shadow vertex</p> * * @param x0 x of point on axis * @param y0 y of point on axis * @param tX x of xFoldP1 point in rotated coordinate system * @param sinA sin value of page curling angle * @param cosA cos value of page curling angel * @param coordX x of texture coordinate * @param coordY y of texture coordinate * @param oX x of originate point * @param oY y of originate point */ private void computeFrontVertex(float x0, float y0, float tX, float sinA, float cosA, float coordX, float coordY, float oX, float oY) { // rotate degree A float x = x0 * cosA - y0 * sinA; float y = x0 * sinA + y0 * cosA; // compute mapping point on cylinder float rad = (x - tX)/ mR; x = (float)(tX + mR * Math.sin(rad)); float cz = (float)(mR * (1 - Math.cos(rad))); // rotate degree -A, sin(-A) = -sin(A), cos(-A) = cos(A) float cx = x * cosA + y * sinA + oX; float cy = y * cosA - x * sinA + oY; mFoldFrontVertexes.addVertex(cx, cy, cz, coordX, coordY); } /** * Compute last vertex of base shadow(backward direction) * <p> * The vertexes of base shadow are composed by two part: forward and * backward part. Forward vertexes are computed from XFold points and * backward vertexes are computed from YFold points. The reason why we use * forward and backward is because how to change float buffer index when we * add a new vertex to buffer. Backward means the index is declined from * buffer middle position to the head, in contrast, the forward is * increasing index from middle to the tail. This design will help keep * float buffer consecutive and to be draw at a time. * </p><p> * Sometimes, the whole or part of YFold points will be outside page, that * means their Y coordinate are greater than page height(diagonal.y). In * this case, we have to crop them like cropping line on 2D coordinate * system. If delve further, we can conclude that we only need to compute * the first start/end vertexes which is falling on the border line of * diagonal.y since other backward vertexes must be outside page and could * not be seen, and then combine these vertexes with forward vertexes to * render base shadow. * </p><p> * This function is just used to compute the couple vertexes. * </p> * * @param x0 x of point on axis * @param y0 y of point on axis * @param tX x of xFoldP1 point in rotated coordinate system * @param sinA sin value of page curling angle * @param cosA cos value of page curling angel * @param baseWcosA base shadow width * cosA * @param baseWsinA base shadow width * sinA * @param oX x of originate point * @param oY y of originate point * @param dY y of diagonal point */ private void computeBaseShadowLastVertex(float x0, float y0, float tX, float sinA, float cosA, float baseWcosA, float baseWsinA, float oX, float oY, float dY) { // like computing front vertex, we firstly compute the mapping vertex // on fold cylinder for point (x0, y0) which also is last vertex of // base shadow(backward direction) float x = x0 * cosA - y0 * sinA; float y = x0 * sinA + y0 * cosA; // compute mapping point on cylinder float rad = (x - tX)/ mR; x = (float)(tX + mR * Math.sin(rad)); float cx1 = x * cosA + y * sinA + oX; float cy1 = y * cosA - x * sinA + oY; // now, we have start vertex(cx1, cy1), compute end vertex(cx2, cy2) // which is translated based on start vertex(cx1, cy1) float cx2 = cx1 + baseWcosA; float cy2 = cy1 - baseWsinA; // as we know, this function is only used to compute last vertex of // base shadow(backward) when the YFold points are outside page height, // that means the (cx1, cy1) and (cx2, cy2) we computed above normally // is outside page, so we need to compute their projection points on page // border as rendering vertex of base shadow float bx1 = cx1 + mKValue * (cy1 - dY); float bx2 = cx2 + mKValue * (cy2 - dY); // add start/end vertex into base shadow buffer, it will be linked with // forward vertexes to draw base shadow mFoldBaseShadow.addVertexes(false, bx1, dY, bx2, dY); } /** * Compute vertexes when page flip is slope */ private void computeVertexesWhenSlope() { final Page page = mPages[FIRST_PAGE]; final float oX = page.originP.x; final float oY = page.originP.y; final float dY = page.diagonalP.y; final float cOX = page.originP.texX; final float cOY = page.originP.texY; final float cDY = page.diagonalP.texY; final float height = page.height; final float d2oY = dY - oY; // compute radius and sin/cos of angle float sinA = (mTouchP.y - oY) / mLenOfTouchOrigin; float cosA = (oX - mTouchP.x) / mLenOfTouchOrigin; // need to translate before rotate, and then translate back int count = mMeshCount; float xFoldP1 = (mXFoldP1.x - oX) * cosA; float edgeW = mFoldEdgesShadowWidth.width(mR); float baseW = mFoldBaseShadowWidth.width(mR); float baseWcosA = baseW * cosA; float baseWsinA = baseW * sinA; float edgeY = oY > 0 ? edgeW : -edgeW; float edgeX = oX > 0 ? edgeW : -edgeW; float stepSY = edgeY / count; float stepSX = edgeX / count; // reset vertexes buffer counter mFoldEdgesShadow.reset(); mFoldBaseShadow.reset(); mFoldFrontVertexes.reset(); mFoldBackVertexes.reset(); // add the first 3 float numbers is fold triangle mFoldBackVertexes.addVertex(mTouchP.x, mTouchP.y, 1, 0, cOX, cOY); // compute vertexes for fold back part float stepX = (mXFoldP0.x - mXFoldP.x) / count; float stepY = (mYFoldP0.y - mYFoldP.y) / count; float x = mXFoldP0.x - oX; float y = mYFoldP0.y - oY; float sx = edgeX; float sy = edgeY; // compute point of back of fold page // Case 1: y coordinate of point YFP0 -> YFP is < diagonalP.y // // <---- Flip // +-------------+ diagonalP // | | // | + YFP // | /| // | / | // | / | // | / | // | / + YFP0 // | / p /| // +------+--.-+-+ originP // XFP XFP0 // // 1. XFP -> XFP0 -> originP -> YFP0 ->YFP is back of fold page // 2. XFP -> XFP0 -> YFP0 -> YFP is a half of cylinder when page is // curled // 3. P point will be computed // // compute points within the page int i = 0; for (;i <= count && Math.abs(y) < height; ++i, x -= stepX, y -= stepY, sy -= stepSY, sx -= stepSX) { computeBackVertex(true, x, 0, x, sy, xFoldP1, sinA, cosA, page.textureX(x + oX), cOY, oX, oY); computeBackVertex(false, 0, y, sx, y, xFoldP1, sinA, cosA, cOX, page.textureY(y + oY), oX, oY); } // If y coordinate of point on YFP0 -> YFP is > diagonalP // There are two cases: // <---- Flip // Case 2 Case 3 // YFP YFP YFP0 // +---------+---+ diagonalP +--------+-----+--+ diagonalP // | / | | / / | // | / + YFP0 | / / | // | / /| | / / | // | / / | | / / | // | / / | | / / | // | / p / | | / p / | // +--+--.--+----+ originalP +-+--.--+---------+ originalP // XFP XFP0 XFP XFP0 // // compute points outside the page if (i <= count) { if (Math.abs(y) != height) { // case 3: compute mapping point of diagonalP if (Math.abs(mYFoldP0.y - oY) > height) { float tx = oX + 2 * mKValue * (mYFoldP.y - dY); float ty = dY + mKValue * (tx - oX); mFoldBackVertexes.addVertex(tx, ty, 1, 0, cOX, cDY); float tsx = tx - sx; float tsy = dY + mKValue * (tsx - oX); mFoldEdgesShadow.addVertexes(false, tx, ty, tsx, tsy); } // case 2: compute mapping point of diagonalP else { float x1 = mKValue * d2oY; computeBackVertex(true, x1, 0, x1, sy, xFoldP1, sinA, cosA, page.textureX(x1 + oX), cOY, oX, oY); computeBackVertex(false, 0, d2oY, sx, d2oY, xFoldP1, sinA, cosA, cOX, cDY, oX, oY); } } // compute the remaining points for (; i <= count; ++i, x -= stepX, y -= stepY, sy -= stepSY, sx -= stepSX) { computeBackVertex(true, x, 0, x, sy, xFoldP1, sinA, cosA, page.textureX(x + oX), cOY, oX, oY); // since the origin Y is beyond page, we need to compute its // projection point on page border and then compute mapping // point on curled cylinder float x1 = mKValue * (y + oY - dY); computeBackVertex(x1, d2oY, xFoldP1, sinA, cosA, page.textureX(x1 + oX), cDY, oX, oY); } } mFoldBackVertexes.toFloatBuffer(); // Like above computation, the below steps are computing vertexes of // front of fold page // Case 1: y coordinate of point YFP -> YFP1 is < diagonalP.y // // <---- Flip // +----------------+ diagonalP // | | // | + YFP1 // | /| // | / | // | / | // | / | // | / + YFP // | / /| // | / / | // | / / + YFP0 // | / / /| // | / p / / | // +-----+--.-+--+--+ originP // XFP1 XFP XFP0 // // 1. XFP -> YFP -> YFP1 ->XFP1 is front of fold page and a half of // cylinder when page is curled. // 2. YFP->XFP is joint line of front and back of fold page // 3. P point will be computed // // compute points within the page stepX = (mXFoldP.x - mXFoldP1.x) / count; stepY = (mYFoldP.y - mYFoldP1.y) / count; x = mXFoldP.x - oX - stepX; y = mYFoldP.y - oY - stepY; int j = 0; for (; j < count && Math.abs(y) < height; ++j, x -= stepX, y -= stepY) { computeFrontVertex(true, x, 0, xFoldP1, sinA, cosA, baseWcosA, baseWsinA, page.textureX(x + oX), cOY, oX, oY, dY); computeFrontVertex(false, 0, y, xFoldP1, sinA, cosA, baseWcosA, baseWsinA, cOX, page.textureY(y + oY), oX, oY, dY); } // compute points outside the page if (j < count) { // compute mapping point of diagonalP if (Math.abs(y) != height && j > 0) { float y1 = (dY - oY); float x1 = mKValue * y1; computeFrontVertex(true, x1, 0, xFoldP1, sinA, cosA, baseWcosA, baseWsinA, page.textureX(x1 + oX), cOY, oX, oY, dY); computeFrontVertex(0, y1, xFoldP1, sinA, cosA, cOX, page.textureY(y1+oY), oX, oY) ; } // compute last pair of vertexes of base shadow computeBaseShadowLastVertex(0, y, xFoldP1, sinA, cosA, baseWcosA, baseWsinA, oX, oY, dY); // compute the remaining points for (; j < count; ++j, x -= stepX, y -= stepY) { computeFrontVertex(true, x, 0, xFoldP1, sinA, cosA, baseWcosA, baseWsinA, page.textureX(x + oX), cOY, oX, oY, dY); float x1 = mKValue * (y + oY - dY); computeFrontVertex(x1, d2oY, xFoldP1, sinA, cosA, page.textureX(x1 + oX), cDY, oX, oY); } } // set uniform Z value for shadow vertexes mFoldEdgesShadow.vertexZ = mFoldFrontVertexes.getFloatAt(2); mFoldBaseShadow.vertexZ = -0.5f; // add two vertexes to connect with the unfold front page page.buildVertexesOfPageWhenSlope(mFoldFrontVertexes, mXFoldP1, mYFoldP1, mKValue); mFoldFrontVertexes.toFloatBuffer(); // compute vertexes of fold edge shadow mFoldBaseShadow.toFloatBuffer(); computeVertexesOfFoldTopEdgeShadow(mTouchP.x, mTouchP.y, sinA, cosA, -edgeX, edgeY); mFoldEdgesShadow.toFloatBuffer(); } /** * Compute vertexes of fold top edge shadow * <p>Top edge shadow of fold page is a quarter circle</p> * * @param x0 X of touch point * @param y0 Y of touch point * @param sinA Sin value of page curling angle * @param cosA Cos value of page curling angle * @param sx Shadow width on X axis * @param sy Shadow width on Y axis */ private void computeVertexesOfFoldTopEdgeShadow(float x0, float y0, float sinA, float cosA, float sx, float sy) { float sin2A = 2 * sinA * cosA; float cos2A = (float)(1 - 2 * Math.pow(sinA, 2)); float r = 0; float dr = (float)(Math.PI / (FOLD_TOP_EDGE_SHADOW_VEX_COUNT - 2)); int size = FOLD_TOP_EDGE_SHADOW_VEX_COUNT / 2; int j = mFoldEdgesShadow.mMaxBackward; // ^ Y __ | // TouchP+ | / | // \ | | | // \ | \ | // \ | X <--------------+--+- OriginP // \| /| // X <----------+--+- OriginP / | // / | / | // | | / | // \__+ Top edge TouchP+ | // | v Y // 1. compute quarter circle at origin point // 2. rotate quarter circle to touch point direction // 3. move quarter circle to touch point as top edge shadow for (int i = 0; i < size; ++i, r += dr, j += 8) { float x = (float)(sx * Math.cos(r)); float y = (float)(sy * Math.sin(r)); // rotate -2A and then translate to touchP mFoldEdgesShadow.setVertexes(j, x0, y0, x * cos2A + y * sin2A + x0, y * cos2A - x * sin2A + y0); } } /** * Compute mesh count for page flip */ private void computeMeshCount() { float dx = Math.abs(mXFoldP0.x - mXFoldP1.x); float dy = Math.abs(mYFoldP0.y - mYFoldP1.y); int len = mIsVertical ? (int)dx : (int)Math.min(dx, dy); mMeshCount = 0; // make sure mesh count is greater than threshold, if less than it, // the page maybe is drawn unsmoothly for (int i = mPixelsOfMesh; i >= 1 && mMeshCount < MESH_COUNT_THRESHOLD; i >>= 1) { mMeshCount = len / i; } // keep count is even if (mMeshCount % 2 != 0) { mMeshCount++; } // half count for fold page mMeshCount >>= 1; } /** * Compute tan value of curling angle * * @param dy the diff value between touchP.y and originP.y * @return tan value of max curl angle */ private float computeTanOfCurlAngle(float dy) { float ratio = dy / mViewRect.halfH; if (ratio <= 1 - MAX_PAGE_CURL_ANGLE_RATIO) { return MAX_PAGE_CURL_TAN_OF_ANGEL; } float degree = MAX_PAGE_CURL_ANGLE - PAGE_CURL_ANGEL_DIFF * ratio; if (degree < MIN_PAGE_CURL_ANGLE) { return MIN_PAGE_CURL_TAN_OF_ANGLE; } else { return (float)Math.tan(Math.PI * degree / 180); } } /** * Debug information */ private void debugInfo() { final GLPoint originP = mPages[FIRST_PAGE].originP; final GLPoint diagonalP = mPages[FIRST_PAGE].diagonalP; Log.d(TAG, "************************************"); Log.d(TAG, " Mesh Count: " + mMeshCount); Log.d(TAG, " Mesh Pixels: " + mPixelsOfMesh); Log.d(TAG, " Origin: " + originP.x + ", " + originP.y); Log.d(TAG, " Diagonal: " + diagonalP.x + ", " + diagonalP.y); Log.d(TAG, " OriginTouchP: " + mStartTouchP.x + ", " + mStartTouchP.y); Log.d(TAG, " TouchP: " + mTouchP.x + ", " + mTouchP.y); Log.d(TAG, " MiddleP: " + mMiddleP.x + ", " + mMiddleP.y); Log.d(TAG, " XFoldP: " + mXFoldP.x + ", " + mXFoldP.y); Log.d(TAG, " XFoldP0: " + mXFoldP0.x + ", " + mXFoldP0.y); Log.d(TAG, " XFoldP1: " + mXFoldP1.x + ", " + mXFoldP1.y); Log.d(TAG, " YFoldP: " + mYFoldP.x + ", " + mYFoldP.y); Log.d(TAG, " YFoldP0: " + mYFoldP0.x + ", " + mYFoldP0.y); Log.d(TAG, " YFoldP1: " + mYFoldP1.x + ", " + mYFoldP1.y); Log.d(TAG, " LengthT->O: " + mLenOfTouchOrigin); } }