pull/37/head
parent
5bfde1b6fe
commit
f7ad79bd0a
@ -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); |
||||||
|
} |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue