pull/37/head
kunfei 5 years ago
parent 5bfde1b6fe
commit f7ad79bd0a
  1. 3
      app/src/main/java/io/legado/app/ui/widget/page/PageView.kt
  2. 957
      app/src/main/java/io/legado/app/ui/widget/page/curl/CurlMesh.java
  3. 195
      app/src/main/java/io/legado/app/ui/widget/page/curl/CurlPage.java
  4. 250
      app/src/main/java/io/legado/app/ui/widget/page/curl/CurlRenderer.java
  5. 790
      app/src/main/java/io/legado/app/ui/widget/page/curl/CurlView.java
  6. 12
      app/src/main/res/layout/dialog_read_book_style.xml

@ -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()

@ -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<ShadowVertex> mArrDropShadowVertices;
private Array<Vertex> mArrIntersections;
private Array<Vertex> mArrOutputVertices;
private Array<Vertex> mArrRotatedVertices;
private Array<Double> mArrScanLines;
private Array<ShadowVertex> mArrSelfShadowVertices;
private Array<ShadowVertex> mArrTempShadowVertices;
private Array<Vertex> mArrTempVertices;
// Buffers for feeding rasterizer.
private FloatBuffer mBufColors;
private FloatBuffer mBufCurlPositionLines;
private FloatBuffer mBufShadowColors;
private FloatBuffer mBufShadowVertices;
private FloatBuffer mBufTexCoords;
private FloatBuffer mBufVertices;
private int mCurlPositionLinesCount;
private int mDropShadowCount;
// Boolean for 'flipping' texture sideways.
private boolean mFlipTexture = false;
// Maximum number of split lines used for creating a curl.
private int mMaxCurlSplits;
// Bounding rectangle for this mesh. mRectagle[0] = top-left corner,
// mRectangle[1] = bottom-left, mRectangle[2] = top-right and mRectangle[3]
// bottom-right.
private final Vertex[] mRectangle = new Vertex[4];
private int mSelfShadowCount;
private boolean mTextureBack = false;
// Texture ids and other variables.
private int[] mTextureIds = null;
private final CurlPage mTexturePage = new CurlPage();
private final RectF mTextureRectBack = new RectF();
private final RectF mTextureRectFront = new RectF();
private int mVerticesCountBack;
private int mVerticesCountFront;
/**
* Constructor for mesh object.
*
* @param maxCurlSplits Maximum number curl can be divided into. The bigger the value
* the smoother curl will be. With the cost of having more
* polygons for drawing.
*/
public CurlMesh(int maxCurlSplits) {
// There really is no use for 0 splits.
mMaxCurlSplits = maxCurlSplits < 1 ? 1 : maxCurlSplits;
mArrScanLines = new Array<Double>(maxCurlSplits + 2);
mArrOutputVertices = new Array<Vertex>(7);
mArrRotatedVertices = new Array<Vertex>(4);
mArrIntersections = new Array<Vertex>(2);
mArrTempVertices = new Array<Vertex>(7 + 4);
for (int i = 0; i < 7 + 4; ++i) {
mArrTempVertices.add(new Vertex());
}
if (DRAW_SHADOW) {
mArrSelfShadowVertices = new Array<ShadowVertex>(
(mMaxCurlSplits + 2) * 2);
mArrDropShadowVertices = new Array<ShadowVertex>(
(mMaxCurlSplits + 2) * 2);
mArrTempShadowVertices = new Array<ShadowVertex>(
(mMaxCurlSplits + 2) * 2);
for (int i = 0; i < (mMaxCurlSplits + 2) * 2; ++i) {
mArrTempShadowVertices.add(new ShadowVertex());
}
}
// Rectangle consists of 4 vertices. Index 0 = top-left, index 1 =
// bottom-left, index 2 = top-right and index 3 = bottom-right.
for (int i = 0; i < 4; ++i) {
mRectangle[i] = new Vertex();
}
// Set up shadow penumbra direction to each vertex. We do fake 'self
// shadow' calculations based on this information.
mRectangle[0].mPenumbraX = mRectangle[1].mPenumbraX = mRectangle[1].mPenumbraY = mRectangle[3].mPenumbraY = -1;
mRectangle[0].mPenumbraY = mRectangle[2].mPenumbraX = mRectangle[2].mPenumbraY = mRectangle[3].mPenumbraX = 1;
if (DRAW_CURL_POSITION) {
mCurlPositionLinesCount = 3;
ByteBuffer hvbb = ByteBuffer
.allocateDirect(mCurlPositionLinesCount * 2 * 2 * 4);
hvbb.order(ByteOrder.nativeOrder());
mBufCurlPositionLines = hvbb.asFloatBuffer();
mBufCurlPositionLines.position(0);
}
// There are 4 vertices from bounding rect, max 2 from adding split line
// to two corners and curl consists of max mMaxCurlSplits lines each
// outputting 2 vertices.
int maxVerticesCount = 4 + 2 + (2 * mMaxCurlSplits);
ByteBuffer vbb = ByteBuffer.allocateDirect(maxVerticesCount * 3 * 4);
vbb.order(ByteOrder.nativeOrder());
mBufVertices = vbb.asFloatBuffer();
mBufVertices.position(0);
if (DRAW_TEXTURE) {
ByteBuffer tbb = ByteBuffer
.allocateDirect(maxVerticesCount * 2 * 4);
tbb.order(ByteOrder.nativeOrder());
mBufTexCoords = tbb.asFloatBuffer();
mBufTexCoords.position(0);
}
ByteBuffer cbb = ByteBuffer.allocateDirect(maxVerticesCount * 4 * 4);
cbb.order(ByteOrder.nativeOrder());
mBufColors = cbb.asFloatBuffer();
mBufColors.position(0);
if (DRAW_SHADOW) {
int maxShadowVerticesCount = (mMaxCurlSplits + 2) * 2 * 2;
ByteBuffer scbb = ByteBuffer
.allocateDirect(maxShadowVerticesCount * 4 * 4);
scbb.order(ByteOrder.nativeOrder());
mBufShadowColors = scbb.asFloatBuffer();
mBufShadowColors.position(0);
ByteBuffer sibb = ByteBuffer
.allocateDirect(maxShadowVerticesCount * 3 * 4);
sibb.order(ByteOrder.nativeOrder());
mBufShadowVertices = sibb.asFloatBuffer();
mBufShadowVertices.position(0);
mDropShadowCount = mSelfShadowCount = 0;
}
}
/**
* Adds vertex to buffers.
*/
private void addVertex(Vertex vertex) {
mBufVertices.put((float) vertex.mPosX);
mBufVertices.put((float) vertex.mPosY);
mBufVertices.put((float) vertex.mPosZ);
mBufColors.put(vertex.mColorFactor * Color.red(vertex.mColor) / 255f);
mBufColors.put(vertex.mColorFactor * Color.green(vertex.mColor) / 255f);
mBufColors.put(vertex.mColorFactor * Color.blue(vertex.mColor) / 255f);
mBufColors.put(Color.alpha(vertex.mColor) / 255f);
if (DRAW_TEXTURE) {
mBufTexCoords.put((float) vertex.mTexX);
mBufTexCoords.put((float) vertex.mTexY);
}
}
/**
* Sets curl for this mesh.
*
* @param curlPos Position for curl 'center'. Can be any point on line collinear
* to curl.
* @param curlDir Curl direction, should be normalized.
* @param radius Radius of curl.
*/
public synchronized void curl(PointF curlPos, PointF curlDir, double radius) {
// First add some 'helper' lines used for development.
if (DRAW_CURL_POSITION) {
mBufCurlPositionLines.position(0);
mBufCurlPositionLines.put(curlPos.x);
mBufCurlPositionLines.put(curlPos.y - 1.0f);
mBufCurlPositionLines.put(curlPos.x);
mBufCurlPositionLines.put(curlPos.y + 1.0f);
mBufCurlPositionLines.put(curlPos.x - 1.0f);
mBufCurlPositionLines.put(curlPos.y);
mBufCurlPositionLines.put(curlPos.x + 1.0f);
mBufCurlPositionLines.put(curlPos.y);
mBufCurlPositionLines.put(curlPos.x);
mBufCurlPositionLines.put(curlPos.y);
mBufCurlPositionLines.put(curlPos.x + curlDir.x * 2);
mBufCurlPositionLines.put(curlPos.y + curlDir.y * 2);
mBufCurlPositionLines.position(0);
}
// Actual 'curl' implementation starts here.
mBufVertices.position(0);
mBufColors.position(0);
if (DRAW_TEXTURE) {
mBufTexCoords.position(0);
}
// Calculate curl angle from direction.
double curlAngle = Math.acos(curlDir.x);
curlAngle = curlDir.y > 0 ? -curlAngle : curlAngle;
// Initiate rotated rectangle which's is translated to curlPos and
// rotated so that curl direction heads to right (1,0). Vertices are
// ordered in ascending order based on x -coordinate at the same time.
// And using y -coordinate in very rare case in which two vertices have
// same x -coordinate.
mArrTempVertices.addAll(mArrRotatedVertices);
mArrRotatedVertices.clear();
for (int i = 0; i < 4; ++i) {
Vertex v = mArrTempVertices.remove(0);
v.set(mRectangle[i]);
v.translate(-curlPos.x, -curlPos.y);
v.rotateZ(-curlAngle);
int j = 0;
for (; j < mArrRotatedVertices.size(); ++j) {
Vertex v2 = mArrRotatedVertices.get(j);
if (v.mPosX > v2.mPosX) {
break;
}
if (v.mPosX == v2.mPosX && v.mPosY > v2.mPosY) {
break;
}
}
mArrRotatedVertices.add(j, v);
}
// Rotated rectangle lines/vertex indices. We need to find bounding
// lines for rotated rectangle. After sorting vertices according to
// their x -coordinate we don't have to worry about vertices at indices
// 0 and 1. But due to inaccuracy it's possible vertex 3 is not the
// opposing corner from vertex 0. So we are calculating distance from
// vertex 0 to vertices 2 and 3 - and altering line indices if needed.
// Also vertices/lines are given in an order first one has x -coordinate
// at least the latter one. This property is used in getIntersections to
// see if there is an intersection.
int lines[][] = {{0, 1}, {0, 2}, {1, 3}, {2, 3}};
{
// TODO: There really has to be more 'easier' way of doing this -
// not including extensive use of sqrt.
Vertex v0 = mArrRotatedVertices.get(0);
Vertex v2 = mArrRotatedVertices.get(2);
Vertex v3 = mArrRotatedVertices.get(3);
double dist2 = Math.sqrt((v0.mPosX - v2.mPosX)
* (v0.mPosX - v2.mPosX) + (v0.mPosY - v2.mPosY)
* (v0.mPosY - v2.mPosY));
double dist3 = Math.sqrt((v0.mPosX - v3.mPosX)
* (v0.mPosX - v3.mPosX) + (v0.mPosY - v3.mPosY)
* (v0.mPosY - v3.mPosY));
if (dist2 > dist3) {
lines[1][1] = 3;
lines[2][1] = 2;
}
}
mVerticesCountFront = mVerticesCountBack = 0;
if (DRAW_SHADOW) {
mArrTempShadowVertices.addAll(mArrDropShadowVertices);
mArrTempShadowVertices.addAll(mArrSelfShadowVertices);
mArrDropShadowVertices.clear();
mArrSelfShadowVertices.clear();
}
// Length of 'curl' curve.
double curlLength = Math.PI * radius;
// Calculate scan lines.
// TODO: Revisit this code one day. There is room for optimization here.
mArrScanLines.clear();
if (mMaxCurlSplits > 0) {
mArrScanLines.add((double) 0);
}
for (int i = 1; i < mMaxCurlSplits; ++i) {
mArrScanLines.add((-curlLength * i) / (mMaxCurlSplits - 1));
}
// As mRotatedVertices is ordered regarding x -coordinate, adding
// this scan line produces scan area picking up vertices which are
// rotated completely. One could say 'until infinity'.
mArrScanLines.add(mArrRotatedVertices.get(3).mPosX - 1);
// Start from right most vertex. Pretty much the same as first scan area
// is starting from 'infinity'.
double scanXmax = mArrRotatedVertices.get(0).mPosX + 1;
for (int i = 0; i < mArrScanLines.size(); ++i) {
// Once we have scanXmin and scanXmax we have a scan area to start
// working with.
double scanXmin = mArrScanLines.get(i);
// First iterate 'original' rectangle vertices within scan area.
for (int j = 0; j < mArrRotatedVertices.size(); ++j) {
Vertex v = mArrRotatedVertices.get(j);
// Test if vertex lies within this scan area.
// TODO: Frankly speaking, can't remember why equality check was
// added to both ends. Guessing it was somehow related to case
// where radius=0f, which, given current implementation, could
// be handled much more effectively anyway.
if (v.mPosX >= scanXmin && v.mPosX <= scanXmax) {
// Pop out a vertex from temp vertices.
Vertex n = mArrTempVertices.remove(0);
n.set(v);
// This is done solely for triangulation reasons. Given a
// rotated rectangle it has max 2 vertices having
// intersection.
Array<Vertex> intersections = getIntersections(
mArrRotatedVertices, lines, n.mPosX);
// In a sense one could say we're adding vertices always in
// two, positioned at the ends of intersecting line. And for
// triangulation to work properly they are added based on y
// -coordinate. And this if-else is doing it for us.
if (intersections.size() == 1
&& intersections.get(0).mPosY > v.mPosY) {
// In case intersecting vertex is higher add it first.
mArrOutputVertices.addAll(intersections);
mArrOutputVertices.add(n);
} else if (intersections.size() <= 1) {
// Otherwise add original vertex first.
mArrOutputVertices.add(n);
mArrOutputVertices.addAll(intersections);
} else {
// There should never be more than 1 intersecting
// vertex. But if it happens as a fallback simply skip
// everything.
mArrTempVertices.add(n);
mArrTempVertices.addAll(intersections);
}
}
}
// Search for scan line intersections.
Array<Vertex> intersections = getIntersections(mArrRotatedVertices,
lines, scanXmin);
// We expect to get 0 or 2 vertices. In rare cases there's only one
// but in general given a scan line intersecting rectangle there
// should be 2 intersecting vertices.
if (intersections.size() == 2) {
// There were two intersections, add them based on y
// -coordinate, higher first, lower last.
Vertex v1 = intersections.get(0);
Vertex v2 = intersections.get(1);
if (v1.mPosY < v2.mPosY) {
mArrOutputVertices.add(v2);
mArrOutputVertices.add(v1);
} else {
mArrOutputVertices.addAll(intersections);
}
} else if (intersections.size() != 0) {
// This happens in a case in which there is a original vertex
// exactly at scan line or something went very much wrong if
// there are 3+ vertices. What ever the reason just return the
// vertices to temp vertices for later use. In former case it
// was handled already earlier once iterating through
// mRotatedVertices, in latter case it's better to avoid doing
// anything with them.
mArrTempVertices.addAll(intersections);
}
// Add vertices found during this iteration to vertex etc buffers.
while (mArrOutputVertices.size() > 0) {
Vertex v = mArrOutputVertices.remove(0);
mArrTempVertices.add(v);
// Local texture front-facing flag.
boolean textureFront;
// Untouched vertices.
if (i == 0) {
textureFront = true;
mVerticesCountFront++;
}
// 'Completely' rotated vertices.
else if (i == mArrScanLines.size() - 1 || curlLength == 0) {
v.mPosX = -(curlLength + v.mPosX);
v.mPosZ = 2 * radius;
v.mPenumbraX = -v.mPenumbraX;
textureFront = false;
mVerticesCountBack++;
}
// Vertex lies within 'curl'.
else {
// Even though it's not obvious from the if-else clause,
// here v.mPosX is between [-curlLength, 0]. And we can do
// calculations around a half cylinder.
double rotY = Math.PI * (v.mPosX / curlLength);
v.mPosX = radius * Math.sin(rotY);
v.mPosZ = radius - (radius * Math.cos(rotY));
v.mPenumbraX *= Math.cos(rotY);
// Map color multiplier to [.1f, 1f] range.
v.mColorFactor = (float) (.1f + .9f * Math.sqrt(Math
.sin(rotY) + 1));
if (v.mPosZ >= radius) {
textureFront = false;
mVerticesCountBack++;
} else {
textureFront = true;
mVerticesCountFront++;
}
}
// We use local textureFront for flipping backside texture
// locally. Plus additionally if mesh is in flip texture mode,
// we'll make the procedure "backwards". Also, until this point,
// texture coordinates are within [0, 1] range so we'll adjust
// them to final texture coordinates too.
if (textureFront != mFlipTexture) {
v.mTexX *= mTextureRectFront.right;
v.mTexY *= mTextureRectFront.bottom;
v.mColor = mTexturePage.getColor(CurlPage.SIDE_FRONT);
} else {
v.mTexX *= mTextureRectBack.right;
v.mTexY *= mTextureRectBack.bottom;
v.mColor = mTexturePage.getColor(CurlPage.SIDE_BACK);
}
// Move vertex back to 'world' coordinates.
v.rotateZ(curlAngle);
v.translate(curlPos.x, curlPos.y);
addVertex(v);
// Drop shadow is cast 'behind' the curl.
if (DRAW_SHADOW && v.mPosZ > 0 && v.mPosZ <= radius) {
ShadowVertex sv = mArrTempShadowVertices.remove(0);
sv.mPosX = v.mPosX;
sv.mPosY = v.mPosY;
sv.mPosZ = v.mPosZ;
sv.mPenumbraX = (v.mPosZ / 2) * -curlDir.x;
sv.mPenumbraY = (v.mPosZ / 2) * -curlDir.y;
sv.mPenumbraColor = v.mPosZ / radius;
int idx = (mArrDropShadowVertices.size() + 1) / 2;
mArrDropShadowVertices.add(idx, sv);
}
// Self shadow is cast partly over mesh.
if (DRAW_SHADOW && v.mPosZ > radius) {
ShadowVertex sv = mArrTempShadowVertices.remove(0);
sv.mPosX = v.mPosX;
sv.mPosY = v.mPosY;
sv.mPosZ = v.mPosZ;
sv.mPenumbraX = ((v.mPosZ - radius) / 3) * v.mPenumbraX;
sv.mPenumbraY = ((v.mPosZ - radius) / 3) * v.mPenumbraY;
sv.mPenumbraColor = (v.mPosZ - radius) / (2 * radius);
int idx = (mArrSelfShadowVertices.size() + 1) / 2;
mArrSelfShadowVertices.add(idx, sv);
}
}
// Switch scanXmin as scanXmax for next iteration.
scanXmax = scanXmin;
}
mBufVertices.position(0);
mBufColors.position(0);
if (DRAW_TEXTURE) {
mBufTexCoords.position(0);
}
// Add shadow Vertices.
if (DRAW_SHADOW) {
mBufShadowColors.position(0);
mBufShadowVertices.position(0);
mDropShadowCount = 0;
for (int i = 0; i < mArrDropShadowVertices.size(); ++i) {
ShadowVertex sv = mArrDropShadowVertices.get(i);
mBufShadowVertices.put((float) sv.mPosX);
mBufShadowVertices.put((float) sv.mPosY);
mBufShadowVertices.put((float) sv.mPosZ);
mBufShadowVertices.put((float) (sv.mPosX + sv.mPenumbraX));
mBufShadowVertices.put((float) (sv.mPosY + sv.mPenumbraY));
mBufShadowVertices.put((float) sv.mPosZ);
for (int j = 0; j < 4; ++j) {
double color = SHADOW_OUTER_COLOR[j]
+ (SHADOW_INNER_COLOR[j] - SHADOW_OUTER_COLOR[j])
* sv.mPenumbraColor;
mBufShadowColors.put((float) color);
}
mBufShadowColors.put(SHADOW_OUTER_COLOR);
mDropShadowCount += 2;
}
mSelfShadowCount = 0;
for (int i = 0; i < mArrSelfShadowVertices.size(); ++i) {
ShadowVertex sv = mArrSelfShadowVertices.get(i);
mBufShadowVertices.put((float) sv.mPosX);
mBufShadowVertices.put((float) sv.mPosY);
mBufShadowVertices.put((float) sv.mPosZ);
mBufShadowVertices.put((float) (sv.mPosX + sv.mPenumbraX));
mBufShadowVertices.put((float) (sv.mPosY + sv.mPenumbraY));
mBufShadowVertices.put((float) sv.mPosZ);
for (int j = 0; j < 4; ++j) {
double color = SHADOW_OUTER_COLOR[j]
+ (SHADOW_INNER_COLOR[j] - SHADOW_OUTER_COLOR[j])
* sv.mPenumbraColor;
mBufShadowColors.put((float) color);
}
mBufShadowColors.put(SHADOW_OUTER_COLOR);
mSelfShadowCount += 2;
}
mBufShadowColors.position(0);
mBufShadowVertices.position(0);
}
}
/**
* Calculates intersections for given scan line.
*/
private Array<Vertex> getIntersections(Array<Vertex> vertices,
int[][] lineIndices, double scanX) {
mArrIntersections.clear();
// Iterate through rectangle lines each re-presented as a pair of
// vertices.
for (int j = 0; j < lineIndices.length; j++) {
Vertex v1 = vertices.get(lineIndices[j][0]);
Vertex v2 = vertices.get(lineIndices[j][1]);
// Here we expect that v1.mPosX >= v2.mPosX and wont do intersection
// test the opposite way.
if (v1.mPosX > scanX && v2.mPosX < scanX) {
// There is an intersection, calculate coefficient telling 'how
// far' scanX is from v2.
double c = (scanX - v2.mPosX) / (v1.mPosX - v2.mPosX);
Vertex n = mArrTempVertices.remove(0);
n.set(v2);
n.mPosX = scanX;
n.mPosY += (v1.mPosY - v2.mPosY) * c;
if (DRAW_TEXTURE) {
n.mTexX += (v1.mTexX - v2.mTexX) * c;
n.mTexY += (v1.mTexY - v2.mTexY) * c;
}
if (DRAW_SHADOW) {
n.mPenumbraX += (v1.mPenumbraX - v2.mPenumbraX) * c;
n.mPenumbraY += (v1.mPenumbraY - v2.mPenumbraY) * c;
}
mArrIntersections.add(n);
}
}
return mArrIntersections;
}
/**
* Getter for textures page for this mesh.
*/
public synchronized CurlPage getTexturePage() {
return mTexturePage;
}
/**
* Renders our page curl mesh.
*/
public synchronized void onDrawFrame(GL10 gl) {
// First allocate texture if there is not one yet.
if (DRAW_TEXTURE && mTextureIds == null) {
// Generate texture.
mTextureIds = new int[2];
gl.glGenTextures(2, mTextureIds, 0);
for (int textureId : mTextureIds) {
// Set texture attributes.
gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId);
gl.glTexParameterf(GL10.GL_TEXTURE_2D,
GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
gl.glTexParameterf(GL10.GL_TEXTURE_2D,
GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_NEAREST);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,
GL10.GL_CLAMP_TO_EDGE);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
GL10.GL_CLAMP_TO_EDGE);
}
}
if (DRAW_TEXTURE && mTexturePage.getTexturesChanged()) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]);
Bitmap texture = mTexturePage.getTexture(mTextureRectFront,
CurlPage.SIDE_FRONT);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, texture, 0);
texture.recycle();
mTextureBack = mTexturePage.hasBackTexture();
if (mTextureBack) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]);
texture = mTexturePage.getTexture(mTextureRectBack,
CurlPage.SIDE_BACK);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, texture, 0);
texture.recycle();
} else {
mTextureRectBack.set(mTextureRectFront);
}
mTexturePage.recycle();
reset();
}
// Some 'global' settings.
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
// TODO: Drop shadow drawing is done temporarily here to hide some
// problems with its calculation.
if (DRAW_SHADOW) {
gl.glDisable(GL10.GL_TEXTURE_2D);
gl.glEnable(GL10.GL_BLEND);
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufShadowColors);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufShadowVertices);
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mDropShadowCount);
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
gl.glDisable(GL10.GL_BLEND);
}
if (DRAW_TEXTURE) {
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, mBufTexCoords);
}
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufVertices);
// Enable color array.
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufColors);
// Draw front facing blank vertices.
gl.glDisable(GL10.GL_TEXTURE_2D);
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mVerticesCountFront);
// Draw front facing texture.
if (DRAW_TEXTURE) {
gl.glEnable(GL10.GL_BLEND);
gl.glEnable(GL10.GL_TEXTURE_2D);
if (!mFlipTexture || !mTextureBack) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]);
} else {
gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]);
}
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mVerticesCountFront);
gl.glDisable(GL10.GL_BLEND);
gl.glDisable(GL10.GL_TEXTURE_2D);
}
int backStartIdx = Math.max(0, mVerticesCountFront - 2);
int backCount = mVerticesCountFront + mVerticesCountBack - backStartIdx;
// Draw back facing blank vertices.
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, backStartIdx, backCount);
// Draw back facing texture.
if (DRAW_TEXTURE) {
gl.glEnable(GL10.GL_BLEND);
gl.glEnable(GL10.GL_TEXTURE_2D);
if (mFlipTexture || !mTextureBack) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]);
} else {
gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]);
}
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, backStartIdx, backCount);
gl.glDisable(GL10.GL_BLEND);
gl.glDisable(GL10.GL_TEXTURE_2D);
}
// Disable textures and color array.
gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
if (DRAW_POLYGON_OUTLINES) {
gl.glEnable(GL10.GL_BLEND);
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glLineWidth(1.0f);
gl.glColor4f(0.5f, 0.5f, 1.0f, 1.0f);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufVertices);
gl.glDrawArrays(GL10.GL_LINE_STRIP, 0, mVerticesCountFront);
gl.glDisable(GL10.GL_BLEND);
}
if (DRAW_CURL_POSITION) {
gl.glEnable(GL10.GL_BLEND);
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glLineWidth(1.0f);
gl.glColor4f(1.0f, 0.5f, 0.5f, 1.0f);
gl.glVertexPointer(2, GL10.GL_FLOAT, 0, mBufCurlPositionLines);
gl.glDrawArrays(GL10.GL_LINES, 0, mCurlPositionLinesCount * 2);
gl.glDisable(GL10.GL_BLEND);
}
if (DRAW_SHADOW) {
gl.glEnable(GL10.GL_BLEND);
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufShadowColors);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufShadowVertices);
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, mDropShadowCount,
mSelfShadowCount);
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
gl.glDisable(GL10.GL_BLEND);
}
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
}
/**
* Resets mesh to 'initial' state. Meaning this mesh will draw a plain
* textured rectangle after call to this method.
*/
public synchronized void reset() {
mBufVertices.position(0);
mBufColors.position(0);
if (DRAW_TEXTURE) {
mBufTexCoords.position(0);
}
for (int i = 0; i < 4; ++i) {
Vertex tmp = mArrTempVertices.get(0);
tmp.set(mRectangle[i]);
if (mFlipTexture) {
tmp.mTexX *= mTextureRectBack.right;
tmp.mTexY *= mTextureRectBack.bottom;
tmp.mColor = mTexturePage.getColor(CurlPage.SIDE_BACK);
} else {
tmp.mTexX *= mTextureRectFront.right;
tmp.mTexY *= mTextureRectFront.bottom;
tmp.mColor = mTexturePage.getColor(CurlPage.SIDE_FRONT);
}
addVertex(tmp);
}
mVerticesCountFront = 4;
mVerticesCountBack = 0;
mBufVertices.position(0);
mBufColors.position(0);
if (DRAW_TEXTURE) {
mBufTexCoords.position(0);
}
mDropShadowCount = mSelfShadowCount = 0;
}
/**
* Resets allocated texture id forcing creation of new one. After calling
* this method you most likely want to set bitmap too as it's lost. This
* method should be called only once e.g GL context is re-created as this
* method does not release previous texture id, only makes sure new one is
* requested on next render.
*/
public synchronized void resetTexture() {
mTextureIds = null;
}
/**
* If true, flips texture sideways.
*/
public synchronized void setFlipTexture(boolean flipTexture) {
mFlipTexture = flipTexture;
if (flipTexture) {
setTexCoords(1f, 0f, 0f, 1f);
} else {
setTexCoords(0f, 0f, 1f, 1f);
}
}
/**
* Update mesh bounds.
*/
public void setRect(RectF r) {
mRectangle[0].mPosX = r.left;
mRectangle[0].mPosY = r.top;
mRectangle[1].mPosX = r.left;
mRectangle[1].mPosY = r.bottom;
mRectangle[2].mPosX = r.right;
mRectangle[2].mPosY = r.top;
mRectangle[3].mPosX = r.right;
mRectangle[3].mPosY = r.bottom;
}
/**
* Sets texture coordinates to mRectangle vertices.
*/
private synchronized void setTexCoords(float left, float top, float right,
float bottom) {
mRectangle[0].mTexX = left;
mRectangle[0].mTexY = top;
mRectangle[1].mTexX = left;
mRectangle[1].mTexY = bottom;
mRectangle[2].mTexX = right;
mRectangle[2].mTexY = top;
mRectangle[3].mTexX = right;
mRectangle[3].mTexY = bottom;
}
/**
* Simple fixed size array implementation.
*/
private class Array<T> {
private Object[] mArray;
private int mCapacity;
private int mSize;
public Array(int capacity) {
mCapacity = capacity;
mArray = new Object[capacity];
}
public void add(int index, T item) {
if (index < 0 || index > mSize || mSize >= mCapacity) {
throw new IndexOutOfBoundsException();
}
for (int i = mSize; i > index; --i) {
mArray[i] = mArray[i - 1];
}
mArray[index] = item;
++mSize;
}
public void add(T item) {
if (mSize >= mCapacity) {
throw new IndexOutOfBoundsException();
}
mArray[mSize++] = item;
}
public void addAll(Array<T> array) {
if (mSize + array.size() > mCapacity) {
throw new IndexOutOfBoundsException();
}
for (int i = 0; i < array.size(); ++i) {
mArray[mSize++] = array.get(i);
}
}
public void clear() {
mSize = 0;
}
@SuppressWarnings("unchecked")
public T get(int index) {
if (index < 0 || index >= mSize) {
throw new IndexOutOfBoundsException();
}
return (T) mArray[index];
}
@SuppressWarnings("unchecked")
public T remove(int index) {
if (index < 0 || index >= mSize) {
throw new IndexOutOfBoundsException();
}
T item = (T) mArray[index];
for (int i = index; i < mSize - 1; ++i) {
mArray[i] = mArray[i + 1];
}
--mSize;
return item;
}
public int size() {
return mSize;
}
}
/**
* Holder for shadow vertex information.
*/
private class ShadowVertex {
public double mPenumbraColor;
public double mPenumbraX;
public double mPenumbraY;
public double mPosX;
public double mPosY;
public double mPosZ;
}
/**
* Holder for vertex information.
*/
private class Vertex {
public int mColor;
public float mColorFactor;
public double mPenumbraX;
public double mPenumbraY;
public double mPosX;
public double mPosY;
public double mPosZ;
public double mTexX;
public double mTexY;
public Vertex() {
mPosX = mPosY = mPosZ = mTexX = mTexY = 0;
mColorFactor = 1.0f;
}
public void rotateZ(double theta) {
double cos = Math.cos(theta);
double sin = Math.sin(theta);
double x = mPosX * cos + mPosY * sin;
double y = mPosX * -sin + mPosY * cos;
mPosX = x;
mPosY = y;
double px = mPenumbraX * cos + mPenumbraY * sin;
double py = mPenumbraX * -sin + mPenumbraY * cos;
mPenumbraX = px;
mPenumbraY = py;
}
public void set(Vertex vertex) {
mPosX = vertex.mPosX;
mPosY = vertex.mPosY;
mPosZ = vertex.mPosZ;
mTexX = vertex.mTexX;
mTexY = vertex.mTexY;
mPenumbraX = vertex.mPenumbraX;
mPenumbraY = vertex.mPenumbraY;
mColor = vertex.mColor;
mColorFactor = vertex.mColorFactor;
}
public void translate(double dx, double dy) {
mPosX += dx;
mPosY += dy;
}
}
}

@ -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;
}
}

@ -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<CurlMesh> mCurlMeshes;
private RectF mMargins = new RectF();
private CurlRenderer.Observer mObserver;
// Page rectangles.
private RectF mPageRectLeft;
private RectF mPageRectRight;
// View mode.
private int mViewMode = SHOW_ONE_PAGE;
// Screen size.
private int mViewportWidth, mViewportHeight;
// Rect for render area.
private RectF mViewRect = new RectF();
/**
* Basic constructor.
*/
public CurlRenderer(CurlRenderer.Observer observer) {
mObserver = observer;
mCurlMeshes = new Vector<CurlMesh>();
mPageRectLeft = new RectF();
mPageRectRight = new RectF();
}
/**
* Adds CurlMesh to this renderer.
*/
public synchronized void addCurlMesh(CurlMesh mesh) {
removeCurlMesh(mesh);
mCurlMeshes.add(mesh);
}
/**
* Returns rect reserved for left or right page. Value page should be
* PAGE_LEFT or PAGE_RIGHT.
*/
public RectF getPageRect(int page) {
if (page == PAGE_LEFT) {
return mPageRectLeft;
} else if (page == PAGE_RIGHT) {
return mPageRectRight;
}
return null;
}
@Override
public synchronized void onDrawFrame(GL10 gl) {
mObserver.onDrawFrame();
gl.glClearColor(Color.red(mBackgroundColor) / 255f,
Color.green(mBackgroundColor) / 255f,
Color.blue(mBackgroundColor) / 255f,
Color.alpha(mBackgroundColor) / 255f);
gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
gl.glLoadIdentity();
if (USE_PERSPECTIVE_PROJECTION) {
gl.glTranslatef(0, 0, -6f);
}
for (int i = 0; i < mCurlMeshes.size(); ++i) {
mCurlMeshes.get(i).onDrawFrame(gl);
}
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
gl.glViewport(0, 0, width, height);
mViewportWidth = width;
mViewportHeight = height;
float ratio = (float) width / height;
mViewRect.top = 1.0f;
mViewRect.bottom = -1.0f;
mViewRect.left = -ratio;
mViewRect.right = ratio;
updatePageRects();
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();
if (USE_PERSPECTIVE_PROJECTION) {
GLU.gluPerspective(gl, 20f, (float) width / height, .1f, 100f);
} else {
GLU.gluOrtho2D(gl, mViewRect.left, mViewRect.right,
mViewRect.bottom, mViewRect.top);
}
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
gl.glClearColor(0f, 0f, 0f, 1f);
gl.glShadeModel(GL10.GL_SMOOTH);
gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
gl.glHint(GL10.GL_LINE_SMOOTH_HINT, GL10.GL_NICEST);
gl.glHint(GL10.GL_POLYGON_SMOOTH_HINT, GL10.GL_NICEST);
gl.glEnable(GL10.GL_LINE_SMOOTH);
gl.glDisable(GL10.GL_DEPTH_TEST);
gl.glDisable(GL10.GL_CULL_FACE);
mObserver.onSurfaceCreated();
}
/**
* Removes CurlMesh from this renderer.
*/
public synchronized void removeCurlMesh(CurlMesh mesh) {
while (mCurlMeshes.remove(mesh))
;
}
/**
* Change background/clear color.
*/
public void setBackgroundColor(int color) {
mBackgroundColor = color;
}
/**
* Set margins or padding. Note: margins are proportional. Meaning a value
* of .1f will produce a 10% margin.
*/
public synchronized void setMargins(float left, float top, float right,
float bottom) {
mMargins.left = left;
mMargins.top = top;
mMargins.right = right;
mMargins.bottom = bottom;
updatePageRects();
}
/**
* Sets visible page count to one or two. Should be either SHOW_ONE_PAGE or
* SHOW_TWO_PAGES.
*/
public synchronized void setViewMode(int viewmode) {
if (viewmode == SHOW_ONE_PAGE) {
mViewMode = viewmode;
updatePageRects();
} else if (viewmode == SHOW_TWO_PAGES) {
mViewMode = viewmode;
updatePageRects();
}
}
/**
* Translates screen coordinates into view coordinates.
*/
public void translate(PointF pt) {
pt.x = mViewRect.left + (mViewRect.width() * pt.x / mViewportWidth);
pt.y = mViewRect.top - (-mViewRect.height() * pt.y / mViewportHeight);
}
/**
* Recalculates page rectangles.
*/
private void updatePageRects() {
if (mViewRect.width() == 0 || mViewRect.height() == 0) {
return;
} else if (mViewMode == SHOW_ONE_PAGE) {
mPageRectRight.set(mViewRect);
mPageRectRight.left += mViewRect.width() * mMargins.left;
mPageRectRight.right -= mViewRect.width() * mMargins.right;
mPageRectRight.top += mViewRect.height() * mMargins.top;
mPageRectRight.bottom -= mViewRect.height() * mMargins.bottom;
mPageRectLeft.set(mPageRectRight);
mPageRectLeft.offset(-mPageRectRight.width(), 0);
int bitmapW = (int) ((mPageRectRight.width() * mViewportWidth) / mViewRect
.width());
int bitmapH = (int) ((mPageRectRight.height() * mViewportHeight) / mViewRect
.height());
mObserver.onPageSizeChanged(bitmapW, bitmapH);
} else if (mViewMode == SHOW_TWO_PAGES) {
mPageRectRight.set(mViewRect);
mPageRectRight.left += mViewRect.width() * mMargins.left;
mPageRectRight.right -= mViewRect.width() * mMargins.right;
mPageRectRight.top += mViewRect.height() * mMargins.top;
mPageRectRight.bottom -= mViewRect.height() * mMargins.bottom;
mPageRectLeft.set(mPageRectRight);
mPageRectLeft.right = (mPageRectLeft.right + mPageRectLeft.left) / 2;
mPageRectRight.left = mPageRectLeft.right;
int bitmapW = (int) ((mPageRectRight.width() * mViewportWidth) / mViewRect
.width());
int bitmapH = (int) ((mPageRectRight.height() * mViewportHeight) / mViewRect
.height());
mObserver.onPageSizeChanged(bitmapW, bitmapH);
}
}
/**
* Observer for waiting render engine/state updates.
*/
public interface Observer {
/**
* Called from onDrawFrame called before rendering is started. This is
* intended to be used for animation purposes.
*/
public void onDrawFrame();
/**
* Called once page size is changed. Width and height tell the page size
* in pixels making it possible to update textures accordingly.
*/
public void onPageSizeChanged(int width, int height);
/**
* Called from onSurfaceCreated to enable texture re-initialization etc
* what needs to be done when this happens.
*/
public void onSurfaceCreated();
}
}

@ -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).
* <p>
* Current index is rounded to closest value divisible with 2.
*/
public void setCurrentIndex(int index) {
if (mPageProvider == null || index < 0) {
mCurrentIndex = 0;
} else {
if (mAllowLastPageCurl) {
mCurrentIndex = Math.min(index, mPageProvider.getPageCount());
} else {
mCurrentIndex = Math.min(index,
mPageProvider.getPageCount() - 1);
}
}
updatePages();
requestRender();
}
/**
* If set to true, touch event pressure information is used to adjust curl
* radius. The more you press, the flatter the curl becomes. This is
* somewhat experimental and results may vary significantly between devices.
* On emulator pressure information seems to be flat 1.0f which is maximum
* value and therefore not very much of use.
*/
public void setEnableTouchPressure(boolean enableTouchPressure) {
mEnableTouchPressure = enableTouchPressure;
}
/**
* Set margins (or padding). Note: margins are proportional. Meaning a value
* of .1f will produce a 10% margin.
*/
public void setMargins(float left, float top, float right, float bottom) {
mRenderer.setMargins(left, top, right, bottom);
}
/**
* Update/set page provider.
*/
public void setPageProvider(PageProvider pageProvider) {
mPageProvider = pageProvider;
mCurrentIndex = 0;
updatePages();
requestRender();
}
/**
* Setter for whether left side page is rendered. This is useful mostly for
* situations where right (main) page is aligned to left side of screen and
* left page is not visible anyway.
*/
public void setRenderLeftPage(boolean renderLeftPage) {
mRenderLeftPage = renderLeftPage;
}
/**
* Sets SizeChangedObserver for this View. Call back method is called from
* this View's onSizeChanged method.
*/
public void setSizeChangedObserver(SizeChangedObserver observer) {
mSizeChangedObserver = observer;
}
/**
* Sets view mode. Value can be either SHOW_ONE_PAGE or SHOW_TWO_PAGES. In
* former case right page is made size of display, and in latter case two
* pages are laid on visible area.
*/
public void setViewMode(int viewMode) {
switch (viewMode) {
case SHOW_ONE_PAGE:
mViewMode = viewMode;
mPageLeft.setFlipTexture(true);
mRenderer.setViewMode(CurlRenderer.SHOW_ONE_PAGE);
break;
case SHOW_TWO_PAGES:
mViewMode = viewMode;
mPageLeft.setFlipTexture(false);
mRenderer.setViewMode(CurlRenderer.SHOW_TWO_PAGES);
break;
}
}
/**
* Switches meshes and loads new bitmaps if available. Updated to support 2
* pages in landscape
*/
private void startCurl(int page) {
switch (page) {
// Once right side page is curled, first right page is assigned into
// curled page. And if there are more bitmaps available new bitmap is
// loaded into right side mesh.
case CURL_RIGHT: {
// Remove meshes from renderer.
mRenderer.removeCurlMesh(mPageLeft);
mRenderer.removeCurlMesh(mPageRight);
mRenderer.removeCurlMesh(mPageCurl);
// We are curling right page.
CurlMesh curl = mPageRight;
mPageRight = mPageCurl;
mPageCurl = curl;
if (mCurrentIndex > 0) {
mPageLeft.setFlipTexture(true);
mPageLeft
.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT));
mPageLeft.reset();
if (mRenderLeftPage) {
mRenderer.addCurlMesh(mPageLeft);
}
}
if (mCurrentIndex < mPageProvider.getPageCount() - 1) {
updatePage(mPageRight.getTexturePage(), mCurrentIndex + 1);
mPageRight.setRect(mRenderer
.getPageRect(CurlRenderer.PAGE_RIGHT));
mPageRight.setFlipTexture(false);
mPageRight.reset();
mRenderer.addCurlMesh(mPageRight);
}
// Add curled page to renderer.
mPageCurl.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT));
mPageCurl.setFlipTexture(false);
mPageCurl.reset();
mRenderer.addCurlMesh(mPageCurl);
mCurlState = CURL_RIGHT;
break;
}
// On left side curl, left page is assigned to curled page. And if
// there are more bitmaps available before currentIndex, new bitmap
// is loaded into left page.
case CURL_LEFT: {
// Remove meshes from renderer.
mRenderer.removeCurlMesh(mPageLeft);
mRenderer.removeCurlMesh(mPageRight);
mRenderer.removeCurlMesh(mPageCurl);
// We are curling left page.
CurlMesh curl = mPageLeft;
mPageLeft = mPageCurl;
mPageCurl = curl;
if (mCurrentIndex > 1) {
updatePage(mPageLeft.getTexturePage(), mCurrentIndex - 2);
mPageLeft.setFlipTexture(true);
mPageLeft
.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT));
mPageLeft.reset();
if (mRenderLeftPage) {
mRenderer.addCurlMesh(mPageLeft);
}
}
// If there is something to show on right page add it to renderer.
if (mCurrentIndex < mPageProvider.getPageCount()) {
mPageRight.setFlipTexture(false);
mPageRight.setRect(mRenderer
.getPageRect(CurlRenderer.PAGE_RIGHT));
mPageRight.reset();
mRenderer.addCurlMesh(mPageRight);
}
// How dragging previous page happens depends on view mode.
if (mViewMode == SHOW_ONE_PAGE
|| (mCurlState == CURL_LEFT && mViewMode == SHOW_TWO_PAGES)) {
mPageCurl.setRect(mRenderer
.getPageRect(CurlRenderer.PAGE_RIGHT));
mPageCurl.setFlipTexture(false);
} else {
mPageCurl
.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT));
mPageCurl.setFlipTexture(true);
}
mPageCurl.reset();
mRenderer.addCurlMesh(mPageCurl);
mCurlState = CURL_LEFT;
break;
}
}
}
/**
* Updates curl position.
*/
private void updateCurlPos(PointerPosition pointerPos) {
// Default curl radius.
double radius = mRenderer.getPageRect(CURL_RIGHT).width() / 3;
// TODO: This is not an optimal solution. Based on feedback received so
// far; pressure is not very accurate, it may be better not to map
// coefficient to range [0f, 1f] but something like [.2f, 1f] instead.
// Leaving it as is until get my hands on a real device. On emulator
// this doesn't work anyway.
radius *= Math.max(1f - pointerPos.mPressure, 0f);
// NOTE: Here we set pointerPos to mCurlPos. It might be a bit confusing
// later to see e.g "mCurlPos.x - mDragStartPos.x" used. But it's
// actually pointerPos we are doing calculations against. Why? Simply to
// optimize code a bit with the cost of making it unreadable. Otherwise
// we had to this in both of the next if-else branches.
mCurlPos.set(pointerPos.mPos);
// If curl happens on right page, or on left page on two page mode,
// we'll calculate curl position from pointerPos.
if (mCurlState == CURL_RIGHT
|| (mCurlState == CURL_LEFT && mViewMode == SHOW_TWO_PAGES)) {
mCurlDir.x = mCurlPos.x - mDragStartPos.x;
mCurlDir.y = mCurlPos.y - mDragStartPos.y;
float dist = (float) Math.sqrt(mCurlDir.x * mCurlDir.x + mCurlDir.y
* mCurlDir.y);
// Adjust curl radius so that if page is dragged far enough on
// opposite side, radius gets closer to zero.
float pageWidth = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)
.width();
double curlLen = radius * Math.PI;
if (dist > (pageWidth * 2) - curlLen) {
curlLen = Math.max((pageWidth * 2) - dist, 0f);
radius = curlLen / Math.PI;
}
// Actual curl position calculation.
if (dist >= curlLen) {
double translate = (dist - curlLen) / 2;
if (mViewMode == SHOW_TWO_PAGES) {
mCurlPos.x -= mCurlDir.x * translate / dist;
} else {
float pageLeftX = mRenderer
.getPageRect(CurlRenderer.PAGE_RIGHT).left;
radius = Math.max(Math.min(mCurlPos.x - pageLeftX, radius),
0f);
}
mCurlPos.y -= mCurlDir.y * translate / dist;
} else {
double angle = Math.PI * Math.sqrt(dist / curlLen);
double translate = radius * Math.sin(angle);
mCurlPos.x += mCurlDir.x * translate / dist;
mCurlPos.y += mCurlDir.y * translate / dist;
}
}
// Otherwise we'll let curl follow pointer position.
else if (mCurlState == CURL_LEFT) {
// Adjust radius regarding how close to page edge we are.
float pageLeftX = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT).left;
radius = Math.max(Math.min(mCurlPos.x - pageLeftX, radius), 0f);
float pageRightX = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT).right;
mCurlPos.x -= Math.min(pageRightX - mCurlPos.x, radius);
mCurlDir.x = mCurlPos.x + mDragStartPos.x;
mCurlDir.y = mCurlPos.y - mDragStartPos.y;
}
setCurlPos(mCurlPos, mCurlDir, radius);
}
/**
* Updates given CurlPage via PageProvider for page located at index.
*/
private void updatePage(CurlPage page, int index) {
// First reset page to initial state.
page.reset();
// Ask page provider to fill it up with bitmaps and colors.
mPageProvider.updatePage(page, mPageBitmapWidth, mPageBitmapHeight,
index);
}
/**
* Updates bitmaps for page meshes.
*/
private void updatePages() {
if (mPageProvider == null || mPageBitmapWidth <= 0
|| mPageBitmapHeight <= 0) {
return;
}
// Remove meshes from renderer.
mRenderer.removeCurlMesh(mPageLeft);
mRenderer.removeCurlMesh(mPageRight);
mRenderer.removeCurlMesh(mPageCurl);
int leftIdx = mCurrentIndex - 1;
int rightIdx = mCurrentIndex;
int curlIdx = -1;
if (mCurlState == CURL_LEFT) {
curlIdx = leftIdx;
--leftIdx;
} else if (mCurlState == CURL_RIGHT) {
curlIdx = rightIdx;
++rightIdx;
}
if (rightIdx >= 0 && rightIdx < mPageProvider.getPageCount()) {
updatePage(mPageRight.getTexturePage(), rightIdx);
mPageRight.setFlipTexture(false);
mPageRight.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT));
mPageRight.reset();
mRenderer.addCurlMesh(mPageRight);
}
if (leftIdx >= 0 && leftIdx < mPageProvider.getPageCount()) {
updatePage(mPageLeft.getTexturePage(), leftIdx);
mPageLeft.setFlipTexture(true);
mPageLeft.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT));
mPageLeft.reset();
if (mRenderLeftPage) {
mRenderer.addCurlMesh(mPageLeft);
}
}
if (curlIdx >= 0 && curlIdx < mPageProvider.getPageCount()) {
updatePage(mPageCurl.getTexturePage(), curlIdx);
if (mCurlState == CURL_RIGHT) {
mPageCurl.setFlipTexture(true);
mPageCurl.setRect(mRenderer
.getPageRect(CurlRenderer.PAGE_RIGHT));
} else {
mPageCurl.setFlipTexture(false);
mPageCurl
.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT));
}
mPageCurl.reset();
mRenderer.addCurlMesh(mPageCurl);
}
}
/**
* Provider for feeding 'book' with bitmaps which are used for rendering
* pages.
*/
public interface PageProvider {
/**
* Return number of pages available.
*/
public int getPageCount();
/**
* Called once new bitmaps/textures are needed. Width and height are in
* pixels telling the size it will be drawn on screen and following them
* ensures that aspect ratio remains. But it's possible to return bitmap
* of any size though. You should use provided CurlPage for storing page
* information for requested page number.<br/>
* <br/>
* Index is a number between 0 and getBitmapCount() - 1.
*/
public void updatePage(CurlPage page, int width, int height, int index);
}
/**
* Simple holder for pointer position.
*/
private class PointerPosition {
PointF mPos = new PointF();
float mPressure;
}
/**
* Observer interface for handling CurlView size changes.
*/
public interface SizeChangedObserver {
/**
* Called once CurlView size changes.
*/
public void onSizeChanged(int width, int height);
}
}

@ -261,6 +261,18 @@
android:singleLine="true"
android:text="@string/page_anim_slide" />
<io.legado.app.lib.theme.view.ATERadioNoButton
android:id="@+id/rb_simulation_anim"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="5dp"
android:padding="5dp"
android:button="@null"
android:gravity="center"
android:singleLine="true"
android:text="@string/page_anim_scroll" />
<io.legado.app.lib.theme.view.ATERadioNoButton
android:id="@+id/rb_scroll_anim"
android:layout_width="0dp"

Loading…
Cancel
Save