diff --git a/app/src/main/java/io/legado/app/ui/widget/page/PageView.kt b/app/src/main/java/io/legado/app/ui/widget/page/PageView.kt index 8b348d310..1c293e74e 100644 --- a/app/src/main/java/io/legado/app/ui/widget/page/PageView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/page/PageView.kt @@ -89,7 +89,8 @@ class PageView(context: Context, attrs: AttributeSet) : pageDelegate = when (context.getPrefInt("pageAnim")) { 0 -> CoverPageDelegate(this) 1 -> SlidePageDelegate(this) - 2 -> ScrollPageDelegate(this) + 2 -> SimulationPageDelegate(this) + 3 -> ScrollPageDelegate(this) else -> NoAnimPageDelegate(this) } upContent() diff --git a/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlMesh.java b/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlMesh.java new file mode 100644 index 000000000..448de5bce --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlMesh.java @@ -0,0 +1,957 @@ +package io.legado.app.ui.widget.page.curl; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.PointF; +import android.graphics.RectF; +import android.opengl.GLUtils; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +import javax.microedition.khronos.opengles.GL10; + +/** + * Class implementing actual curl/page rendering. + * + * @author harism + */ +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 mArrDropShadowVertices; + private Array mArrIntersections; + private Array mArrOutputVertices; + private Array mArrRotatedVertices; + private Array mArrScanLines; + private Array mArrSelfShadowVertices; + private Array mArrTempShadowVertices; + private Array 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(maxCurlSplits + 2); + mArrOutputVertices = new Array(7); + mArrRotatedVertices = new Array(4); + mArrIntersections = new Array(2); + mArrTempVertices = new Array(7 + 4); + for (int i = 0; i < 7 + 4; ++i) { + mArrTempVertices.add(new Vertex()); + } + + if (DRAW_SHADOW) { + mArrSelfShadowVertices = new Array( + (mMaxCurlSplits + 2) * 2); + mArrDropShadowVertices = new Array( + (mMaxCurlSplits + 2) * 2); + mArrTempShadowVertices = new Array( + (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 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 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 getIntersections(Array 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 { + 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 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; + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlPage.java b/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlPage.java new file mode 100644 index 000000000..e2ea847db --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlPage.java @@ -0,0 +1,195 @@ +package io.legado.app.ui.widget.page.curl; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.RectF; + +/** + * Storage class for page textures, blend colors and possibly some other values + * in the future. + * + * @author harism + */ +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; + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlRenderer.java b/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlRenderer.java new file mode 100644 index 000000000..7b18c744d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlRenderer.java @@ -0,0 +1,250 @@ +package io.legado.app.ui.widget.page.curl; + +import android.graphics.Color; +import android.graphics.PointF; +import android.graphics.RectF; +import android.opengl.GLSurfaceView; +import android.opengl.GLU; + +import java.util.Vector; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +/** + * Actual renderer class. + * + * @author harism + */ +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 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(); + 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(); + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlView.java b/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlView.java new file mode 100644 index 000000000..c2428513c --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlView.java @@ -0,0 +1,790 @@ +package io.legado.app.ui.widget.page.curl; + +import android.content.Context; +import android.graphics.PixelFormat; +import android.graphics.PointF; +import android.graphics.RectF; +import android.opengl.GLSurfaceView; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +/** + * OpenGL ES View. + * + * @author harism + */ +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) { + setEGLConfigChooser(8, 8, 8, 8, 16, 0); + getHolder().setFormat(PixelFormat.TRANSLUCENT); + setZOrderOnTop(true); + 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 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.
+ *
+ * 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); + } + +} diff --git a/app/src/main/res/layout/dialog_read_book_style.xml b/app/src/main/res/layout/dialog_read_book_style.xml index f8c997797..49747179b 100644 --- a/app/src/main/res/layout/dialog_read_book_style.xml +++ b/app/src/main/res/layout/dialog_read_book_style.xml @@ -261,6 +261,18 @@ android:singleLine="true" android:text="@string/page_anim_slide" /> + +