pull/63/head
kunfei 5 years ago
parent 41258272b7
commit 81ac4d23d2
  1. 6
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  2. 954
      app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlMesh.java
  3. 191
      app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlPage.kt
  4. 221
      app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlRenderer.kt
  5. 769
      app/src/main/java/io/legado/app/ui/book/read/page/curl/CurlView.kt
  6. 4
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt
  7. 570
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt

@ -9,7 +9,6 @@ import android.widget.FrameLayout
import io.legado.app.constant.PreferKey
import io.legado.app.help.ReadBookConfig
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.curl.CurlView
import io.legado.app.ui.book.read.page.delegate.*
import io.legado.app.utils.activity
import io.legado.app.utils.getPrefInt
@ -26,7 +25,6 @@ class PageView(context: Context, attrs: AttributeSet) :
var prevPage: ContentView? = null
var curPage: ContentView? = null
var nextPage: ContentView? = null
var curlView: CurlView? = null
init {
callBack = activity as? CallBack
@ -91,10 +89,6 @@ class PageView(context: Context, attrs: AttributeSet) :
}
fun upPageAnim(pageAnim: Int) {
if (curlView != null) {
removeView(curlView)
curlView = null
}
pageDelegate = null
pageDelegate = when (pageAnim) {
0 -> CoverPageDelegate(this)

@ -1,954 +0,0 @@
package io.legado.app.ui.book.read.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<>(maxCurlSplits + 2);
mArrOutputVertices = new Array<>(7);
mArrRotatedVertices = new Array<>(4);
mArrIntersections = new Array<>(2);
mArrTempVertices = new Array<>(7 + 4);
for (int i = 0; i < 7 + 4; ++i) {
mArrTempVertices.add(new Vertex());
}
if (DRAW_SHADOW) {
mArrSelfShadowVertices = new Array<>(
(mMaxCurlSplits + 2) * 2);
mArrDropShadowVertices = new Array<>(
(mMaxCurlSplits + 2) * 2);
mArrTempShadowVertices = new Array<>(
(mMaxCurlSplits + 2) * 2);
for (int i = 0; i < (mMaxCurlSplits + 2) * 2; ++i) {
mArrTempShadowVertices.add(new ShadowVertex());
}
}
// Rectangle consists of 4 vertices. Index 0 = top-left, index 1 =
// bottom-left, index 2 = top-right and index 3 = bottom-right.
for (int i = 0; i < 4; ++i) {
mRectangle[i] = new Vertex();
}
// Set up shadow penumbra direction to each vertex. We do fake 'self
// shadow' calculations based on this information.
mRectangle[0].mPenumbraX = mRectangle[1].mPenumbraX = mRectangle[1].mPenumbraY = mRectangle[3].mPenumbraY = -1;
mRectangle[0].mPenumbraY = mRectangle[2].mPenumbraX = mRectangle[2].mPenumbraY = mRectangle[3].mPenumbraX = 1;
if (DRAW_CURL_POSITION) {
mCurlPositionLinesCount = 3;
ByteBuffer hvbb = ByteBuffer
.allocateDirect(mCurlPositionLinesCount * 2 * 2 * 4);
hvbb.order(ByteOrder.nativeOrder());
mBufCurlPositionLines = hvbb.asFloatBuffer();
mBufCurlPositionLines.position(0);
}
// There are 4 vertices from bounding rect, max 2 from adding split line
// to two corners and curl consists of max mMaxCurlSplits lines each
// outputting 2 vertices.
int maxVerticesCount = 4 + 2 + (2 * mMaxCurlSplits);
ByteBuffer vbb = ByteBuffer.allocateDirect(maxVerticesCount * 3 * 4);
vbb.order(ByteOrder.nativeOrder());
mBufVertices = vbb.asFloatBuffer();
mBufVertices.position(0);
if (DRAW_TEXTURE) {
ByteBuffer tbb = ByteBuffer
.allocateDirect(maxVerticesCount * 2 * 4);
tbb.order(ByteOrder.nativeOrder());
mBufTexCoords = tbb.asFloatBuffer();
mBufTexCoords.position(0);
}
ByteBuffer cbb = ByteBuffer.allocateDirect(maxVerticesCount * 4 * 4);
cbb.order(ByteOrder.nativeOrder());
mBufColors = cbb.asFloatBuffer();
mBufColors.position(0);
if (DRAW_SHADOW) {
int maxShadowVerticesCount = (mMaxCurlSplits + 2) * 2 * 2;
ByteBuffer scbb = ByteBuffer
.allocateDirect(maxShadowVerticesCount * 4 * 4);
scbb.order(ByteOrder.nativeOrder());
mBufShadowColors = scbb.asFloatBuffer();
mBufShadowColors.position(0);
ByteBuffer sibb = ByteBuffer
.allocateDirect(maxShadowVerticesCount * 3 * 4);
sibb.order(ByteOrder.nativeOrder());
mBufShadowVertices = sibb.asFloatBuffer();
mBufShadowVertices.position(0);
mDropShadowCount = mSelfShadowCount = 0;
}
}
/**
* Adds vertex to buffers.
*/
private void addVertex(Vertex vertex) {
mBufVertices.put((float) vertex.mPosX);
mBufVertices.put((float) vertex.mPosY);
mBufVertices.put((float) vertex.mPosZ);
mBufColors.put(vertex.mColorFactor * Color.red(vertex.mColor) / 255f);
mBufColors.put(vertex.mColorFactor * Color.green(vertex.mColor) / 255f);
mBufColors.put(vertex.mColorFactor * Color.blue(vertex.mColor) / 255f);
mBufColors.put(Color.alpha(vertex.mColor) / 255f);
if (DRAW_TEXTURE) {
mBufTexCoords.put((float) vertex.mTexX);
mBufTexCoords.put((float) vertex.mTexY);
}
}
/**
* Sets curl for this mesh.
*
* @param curlPos Position for curl 'center'. Can be any point on line collinear
* to curl.
* @param curlDir Curl direction, should be normalized.
* @param radius Radius of curl.
*/
public synchronized void curl(PointF curlPos, PointF curlDir, double radius) {
// First add some 'helper' lines used for development.
if (DRAW_CURL_POSITION) {
mBufCurlPositionLines.position(0);
mBufCurlPositionLines.put(curlPos.x);
mBufCurlPositionLines.put(curlPos.y - 1.0f);
mBufCurlPositionLines.put(curlPos.x);
mBufCurlPositionLines.put(curlPos.y + 1.0f);
mBufCurlPositionLines.put(curlPos.x - 1.0f);
mBufCurlPositionLines.put(curlPos.y);
mBufCurlPositionLines.put(curlPos.x + 1.0f);
mBufCurlPositionLines.put(curlPos.y);
mBufCurlPositionLines.put(curlPos.x);
mBufCurlPositionLines.put(curlPos.y);
mBufCurlPositionLines.put(curlPos.x + curlDir.x * 2);
mBufCurlPositionLines.put(curlPos.y + curlDir.y * 2);
mBufCurlPositionLines.position(0);
}
// Actual 'curl' implementation starts here.
mBufVertices.position(0);
mBufColors.position(0);
if (DRAW_TEXTURE) {
mBufTexCoords.position(0);
}
// Calculate curl angle from direction.
double curlAngle = Math.acos(curlDir.x);
curlAngle = curlDir.y > 0 ? -curlAngle : curlAngle;
// Initiate rotated rectangle which's is translated to curlPos and
// rotated so that curl direction heads to right (1,0). Vertices are
// ordered in ascending order based on x -coordinate at the same time.
// And using y -coordinate in very rare case in which two vertices have
// same x -coordinate.
mArrTempVertices.addAll(mArrRotatedVertices);
mArrRotatedVertices.clear();
for (int i = 0; i < 4; ++i) {
Vertex v = mArrTempVertices.remove(0);
v.set(mRectangle[i]);
v.translate(-curlPos.x, -curlPos.y);
v.rotateZ(-curlAngle);
int j = 0;
for (; j < mArrRotatedVertices.size(); ++j) {
Vertex v2 = mArrRotatedVertices.get(j);
if (v.mPosX > v2.mPosX) {
break;
}
if (v.mPosX == v2.mPosX && v.mPosY > v2.mPosY) {
break;
}
}
mArrRotatedVertices.add(j, v);
}
// Rotated rectangle lines/vertex indices. We need to find bounding
// lines for rotated rectangle. After sorting vertices according to
// their x -coordinate we don't have to worry about vertices at indices
// 0 and 1. But due to inaccuracy it's possible vertex 3 is not the
// opposing corner from vertex 0. So we are calculating distance from
// vertex 0 to vertices 2 and 3 - and altering line indices if needed.
// Also vertices/lines are given in an order first one has x -coordinate
// at least the latter one. This property is used in getIntersections to
// see if there is an intersection.
int[][] lines = {{0, 1}, {0, 2}, {1, 3}, {2, 3}};
{
// TODO: There really has to be more 'easier' way of doing this -
// not including extensive use of sqrt.
Vertex v0 = mArrRotatedVertices.get(0);
Vertex v2 = mArrRotatedVertices.get(2);
Vertex v3 = mArrRotatedVertices.get(3);
double dist2 = Math.sqrt((v0.mPosX - v2.mPosX)
* (v0.mPosX - v2.mPosX) + (v0.mPosY - v2.mPosY)
* (v0.mPosY - v2.mPosY));
double dist3 = Math.sqrt((v0.mPosX - v3.mPosX)
* (v0.mPosX - v3.mPosX) + (v0.mPosY - v3.mPosY)
* (v0.mPosY - v3.mPosY));
if (dist2 > dist3) {
lines[1][1] = 3;
lines[2][1] = 2;
}
}
mVerticesCountFront = mVerticesCountBack = 0;
if (DRAW_SHADOW) {
mArrTempShadowVertices.addAll(mArrDropShadowVertices);
mArrTempShadowVertices.addAll(mArrSelfShadowVertices);
mArrDropShadowVertices.clear();
mArrSelfShadowVertices.clear();
}
// Length of 'curl' curve.
double curlLength = Math.PI * radius;
// Calculate scan lines.
// TODO: Revisit this code one day. There is room for optimization here.
mArrScanLines.clear();
if (mMaxCurlSplits > 0) {
mArrScanLines.add((double) 0);
}
for (int i = 1; i < mMaxCurlSplits; ++i) {
mArrScanLines.add((-curlLength * i) / (mMaxCurlSplits - 1));
}
// As mRotatedVertices is ordered regarding x -coordinate, adding
// this scan line produces scan area picking up vertices which are
// rotated completely. One could say 'until infinity'.
mArrScanLines.add(mArrRotatedVertices.get(3).mPosX - 1);
// Start from right most vertex. Pretty much the same as first scan area
// is starting from 'infinity'.
double scanXmax = mArrRotatedVertices.get(0).mPosX + 1;
for (int i = 0; i < mArrScanLines.size(); ++i) {
// Once we have scanXmin and scanXmax we have a scan area to start
// working with.
double scanXmin = mArrScanLines.get(i);
// First iterate 'original' rectangle vertices within scan area.
for (int j = 0; j < mArrRotatedVertices.size(); ++j) {
Vertex v = mArrRotatedVertices.get(j);
// Test if vertex lies within this scan area.
// TODO: Frankly speaking, can't remember why equality check was
// added to both ends. Guessing it was somehow related to case
// where radius=0f, which, given current implementation, could
// be handled much more effectively anyway.
if (v.mPosX >= scanXmin && v.mPosX <= scanXmax) {
// Pop out a vertex from temp vertices.
Vertex n = mArrTempVertices.remove(0);
n.set(v);
// This is done solely for triangulation reasons. Given a
// rotated rectangle it has max 2 vertices having
// intersection.
Array<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[] lineIndex : lineIndices) {
Vertex v1 = vertices.get(lineIndex[0]);
Vertex v2 = vertices.get(lineIndex[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;
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();
}
System.arraycopy(mArray, index, mArray, index + 1, mSize - index);
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];
if (mSize - 1 - index >= 0)
System.arraycopy(mArray, index + 1, mArray, index, mSize - 1 - index);
--mSize;
return item;
}
public int size() {
return mSize;
}
}
/**
* Holder for shadow vertex information.
*/
private class ShadowVertex {
double mPenumbraColor;
double mPenumbraX;
double mPenumbraY;
double mPosX;
double mPosY;
double mPosZ;
}
/**
* Holder for vertex information.
*/
private class Vertex {
int mColor;
float mColorFactor;
double mPenumbraX;
double mPenumbraY;
double mPosX;
double mPosY;
double mPosZ;
double mTexX;
double mTexY;
Vertex() {
mPosX = mPosY = mPosZ = mTexX = mTexY = 0;
mColorFactor = 1.0f;
}
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;
}
}
}

@ -1,191 +0,0 @@
package io.legado.app.ui.book.read.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
*/
class CurlPage {
private var mColorBack: Int = 0
private var mColorFront: Int = 0
private var mTextureBack: Bitmap? = null
private var mTextureFront: Bitmap? = null
/**
* Returns true if textures have changed.
*/
var texturesChanged: Boolean = false
private set
/**
* Default constructor.
*/
init {
reset()
}
/**
* Getter for color.
*/
fun getColor(side: Int): Int {
return when (side) {
SIDE_FRONT -> mColorFront
else -> mColorBack
}
}
/**
* Calculates the next highest power of two for a given integer.
*/
private fun getNextHighestPO2(n: Int): Int {
var n1 = n
n1 -= 1
n1 = n1 or (n1 shr 1)
n1 = n1 or (n1 shr 2)
n1 = n1 or (n1 shr 4)
n1 = n1 or (n1 shr 8)
n1 = n1 or (n1 shr 16)
n1 = n1 or (n1 shr 32)
return n1 + 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 fun getTexture(bitmap: Bitmap, textureRect: RectF): Bitmap {
// Bitmap original size.
val w = bitmap.width
val h = bitmap.height
// 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.
val newW = getNextHighestPO2(w)
val newH = getNextHighestPO2(h)
// TODO: Is there another way to create a bigger Bitmap and copy
// original Bitmap to it more efficiently? Immutable bitmap anyone?
val bitmapTex = Bitmap.createBitmap(newW, newH, bitmap.config)
val c = Canvas(bitmapTex)
c.drawBitmap(bitmap, 0f, 0f, null)
// Calculate final texture coordinates.
val texX = w.toFloat() / newW
val texY = h.toFloat() / 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.
*/
fun getTexture(textureRect: RectF, side: Int): Bitmap {
return when (side) {
SIDE_FRONT -> getTexture(mTextureFront!!, textureRect)
else -> getTexture(mTextureBack!!, textureRect)
}
}
/**
* Returns true if back siding texture exists and it differs from front
* facing one.
*/
fun hasBackTexture(): Boolean {
return mTextureFront != mTextureBack
}
/**
* Recycles and frees underlying Bitmaps.
*/
fun 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)
texturesChanged = false
}
/**
* Resets this CurlPage into its initial state.
*/
fun reset() {
mColorBack = Color.WHITE
mColorFront = Color.WHITE
recycle()
}
/**
* Setter blend color.
*/
fun setColor(color: Int, side: Int) {
when (side) {
SIDE_FRONT -> mColorFront = color
SIDE_BACK -> mColorBack = color
else -> {
mColorBack = color
mColorFront = mColorBack
}
}
}
/**
* Setter for textures.
*/
fun setTexture(texture: Bitmap?, side: Int) {
var texture1 = texture
if (texture1 == null) {
texture1 = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565)
if (side == SIDE_BACK) {
texture1!!.eraseColor(mColorBack)
} else {
texture1!!.eraseColor(mColorFront)
}
}
when (side) {
SIDE_FRONT -> {
if (mTextureFront != null)
mTextureFront!!.recycle()
mTextureFront = texture1
}
SIDE_BACK -> {
if (mTextureBack != null)
mTextureBack!!.recycle()
mTextureBack = texture1
}
SIDE_BOTH -> {
if (mTextureFront != null)
mTextureFront!!.recycle()
if (mTextureBack != null)
mTextureBack!!.recycle()
mTextureBack = texture1
mTextureFront = mTextureBack
}
}
texturesChanged = true
}
companion object {
const val SIDE_BACK = 2
const val SIDE_BOTH = 3
const val SIDE_FRONT = 1
}
}

@ -1,221 +0,0 @@
package io.legado.app.ui.book.read.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.*
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10
/**
* Actual renderer class.
*
* @author harism
*/
class CurlRenderer(private val mObserver: Observer) : GLSurfaceView.Renderer {
// Background fill color.
private var mBackgroundColor: Int = 0
// Curl meshes used for static and dynamic rendering.
private val mCurlMeshes: Vector<CurlMesh> = Vector()
private val mMargins = RectF()
// Page rectangles.
private val mPageRectLeft: RectF = RectF()
private val mPageRectRight: RectF = RectF()
// View mode.
private var mViewMode = SHOW_ONE_PAGE
// Screen size.
private var mViewportWidth: Int = 0
private var mViewportHeight: Int = 0
// Rect for render area.
private val mViewRect = RectF()
/**
* Adds CurlMesh to this renderer.
*/
@Synchronized
fun addCurlMesh(mesh: CurlMesh) {
removeCurlMesh(mesh)
mCurlMeshes.add(mesh)
}
/**
* Returns rect reserved for left or right page. Value page should be
* PAGE_LEFT or PAGE_RIGHT.
*/
fun getPageRect(page: Int): RectF? {
if (page == PAGE_LEFT) {
return mPageRectLeft
} else if (page == PAGE_RIGHT) {
return mPageRectRight
}
return null
}
@Synchronized
override fun onDrawFrame(gl: GL10) {
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 (!mObserver.canDraw) {
return
}
for (i in mCurlMeshes.indices) {
mCurlMeshes[i].onDrawFrame(gl)
}
}
override fun onSurfaceChanged(gl: GL10, width: Int, height: Int) {
gl.glViewport(0, 0, width, height)
mViewportWidth = width
mViewportHeight = height
val ratio = width.toFloat() / height
mViewRect.top = 1.0f
mViewRect.bottom = -1.0f
mViewRect.left = -ratio
mViewRect.right = ratio
updatePageRect()
gl.glMatrixMode(GL10.GL_PROJECTION)
gl.glLoadIdentity()
GLU.gluOrtho2D(
gl, mViewRect.left, mViewRect.right,
mViewRect.bottom, mViewRect.top
)
gl.glMatrixMode(GL10.GL_MODELVIEW)
gl.glLoadIdentity()
}
override fun onSurfaceCreated(gl: GL10, config: EGLConfig) {
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.
*/
@Synchronized
fun removeCurlMesh(mesh: CurlMesh) {
mCurlMeshes.remove(mesh)
}
/**
* Sets visible page count to one or two. Should be either SHOW_ONE_PAGE or
* SHOW_TWO_PAGES.
*/
@Synchronized
fun setViewMode(viewMode: Int) {
if (viewMode == SHOW_ONE_PAGE) {
mViewMode = viewMode
updatePageRect()
} else if (viewMode == SHOW_TWO_PAGES) {
mViewMode = viewMode
updatePageRect()
}
}
/**
* Translates screen coordinates into view coordinates.
*/
fun translate(pt: PointF) {
pt.x = mViewRect.left + mViewRect.width() * pt.x / mViewportWidth
pt.y = mViewRect.top - -mViewRect.height() * pt.y / mViewportHeight
}
/**
* Recalculates page rectangles.
*/
private fun updatePageRect() {
if (mViewRect.width() == 0f || mViewRect.height() == 0f) {
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(), 0f)
val bitmapW = (mPageRectRight.width() * mViewportWidth / mViewRect
.width()).toInt()
val bitmapH = (mPageRectRight.height() * mViewportHeight / mViewRect
.height()).toInt()
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
val bitmapW = (mPageRectRight.width() * mViewportWidth / mViewRect
.width()).toInt()
val bitmapH = (mPageRectRight.height() * mViewportHeight / mViewRect
.height()).toInt()
mObserver.onPageSizeChanged(bitmapW, bitmapH)
}
}
/**
* Observer for waiting render engine/state updates.
*/
interface Observer {
/**
* Called from onDrawFrame called before rendering is started. This is
* intended to be used for animation purposes.
*/
fun onDrawFrame()
/**
* Called once page size is changed. Width and height tell the page size
* in pixels making it possible to update textures accordingly.
*/
fun onPageSizeChanged(width: Int, height: Int)
/**
* Called from onSurfaceCreated to enable texture re-initialization etc
* what needs to be done when this happens.
*/
fun onSurfaceCreated()
var canDraw: Boolean
}
companion object {
// Constant for requesting left page rect.
const val PAGE_LEFT = 1
// Constant for requesting right page rect.
const val PAGE_RIGHT = 2
// Constants for changing view mode.
const val SHOW_ONE_PAGE = 1
const val SHOW_TWO_PAGES = 2
}
}

@ -1,769 +0,0 @@
package io.legado.app.ui.book.read.page.curl
import android.content.Context
import android.graphics.PixelFormat
import android.graphics.PointF
import android.opengl.GLSurfaceView
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sin
import kotlin.math.sqrt
/**
* OpenGL ES View.
*
* @author harism
*/
class CurlView : GLSurfaceView, View.OnTouchListener, CurlRenderer.Observer {
var callBack: CallBack? = null
private var mAllowLastPageCurl = true
private var mAnimate = false
private val mAnimationDurationTime: Long = 300
private val mAnimationSource = PointF()
private var mAnimationStartTime: Long = 0
private val mAnimationTarget = PointF()
private var mAnimationTargetEvent: Int = 0
private val mCurlDir = PointF()
private val mCurlPos = PointF()
private var mCurlState = CURL_NONE
// Current bitmap index. This is always showed as front of right page.
private var mCurrentIndex = 0
// Start position for dragging.
private val mDragStartPos = PointF()
private var mEnableTouchPressure = false
// Bitmap size. These are updated from renderer once it's initialized.
private var mPageBitmapHeight = -1
private var mPageBitmapWidth = -1
// Page meshes. Left and right meshes are 'static' while curl is used to
// show page flipping.
private var mPageCurl: CurlMesh
private var mPageLeft: CurlMesh
private var mPageRight: CurlMesh
private val mPointerPos = PointerPosition()
private var mRenderer: CurlRenderer = CurlRenderer(this)
private var mRenderLeftPage = true
private var mSizeChangedObserver: SizeChangedObserver? = null
// One page is the default.
private var mViewMode = SHOW_ONE_PAGE
var mPageProvider: PageProvider? = null
set(value) {
field = value
mCurrentIndex = 0
updatePages()
requestRender()
}
/**
* Get current page index. Page indices are zero based values presenting
* page being shown on right side of the book.
*/
/**
* Set current page index. Page indices are zero based values presenting
* page being shown on right side of the book. E.g if you set value to 4;
* right side front facing bitmap will be with index 4, back facing 5 and
* for left side page index 3 is front facing, and index 2 back facing (once
* page is on left side it's flipped over).
*
*
* Current index is rounded to closest value divisible with 2.
*/
var currentIndex: Int
get() = mCurrentIndex
set(index) {
mCurrentIndex = if (mPageProvider == null || index < 0) {
0
} else {
if (mAllowLastPageCurl) {
min(index, mPageProvider!!.pageCount)
} else {
min(index, mPageProvider!!.pageCount - 1)
}
}
updatePages()
requestRender()
}
/**
* Default constructor.
*/
constructor(ctx: Context) : super(ctx)
/**
* Default constructor.
*/
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
/**
* Default constructor.
*/
constructor(ctx: Context, attrs: AttributeSet, defStyle: Int) : this(ctx, attrs)
/**
* Initialize method.
*/
init {
setEGLConfigChooser(8, 8, 8, 8, 16, 0)
holder.setFormat(PixelFormat.TRANSLUCENT)
setZOrderOnTop(true)
setRenderer(mRenderer)
renderMode = 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 = CurlMesh(10)
mPageRight = CurlMesh(10)
mPageCurl = CurlMesh(10)
mPageLeft.setFlipTexture(true)
mPageRight.setFlipTexture(false)
}
override var canDraw: Boolean = false
override fun onDrawFrame() {
// We are not animating.
if (!mAnimate) {
return
}
val currentTime = System.currentTimeMillis()
// If animation is done.
if (currentTime >= mAnimationStartTime + mAnimationDurationTime) {
if (mAnimationTargetEvent == SET_CURL_TO_RIGHT) {
// Switch curled page to right.
val right = mPageCurl
val 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
callBack?.pageChange(-1)
}
canDraw = false
} else if (mAnimationTargetEvent == SET_CURL_TO_LEFT) {
// Switch curled page to left.
val left = mPageCurl
val 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
callBack?.pageChange(1)
}
canDraw = false
}
mCurlState = CURL_NONE
mAnimate = false
requestRender()
} else {
mPointerPos.mPos.set(mAnimationSource)
var t = 1f - (currentTime - mAnimationStartTime).toFloat() / 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 fun onPageSizeChanged(width: Int, height: Int) {
mPageBitmapWidth = width
mPageBitmapHeight = height
updatePages()
requestRender()
}
public override fun onSizeChanged(w: Int, h: Int, ow: Int, oh: Int) {
super.onSizeChanged(w, h, ow, oh)
requestRender()
if (mSizeChangedObserver != null) {
mSizeChangedObserver!!.onSizeChanged(w, h)
}
}
override fun 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 fun onTouch(view: View, me: MotionEvent): Boolean {
// No dragging during animation at the moment.
if (mAnimate || mPageProvider == null) {
return false
}
// We need page rects quite extensively so get them for later use.
val rightRect = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)
val leftRect = mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)
// Store pointer position.
mPointerPos.mPos.set(me.x, me.y)
mRenderer.translate(mPointerPos.mPos)
if (mEnableTouchPressure) {
mPointerPos.mPressure = me.pressure
} else {
mPointerPos.mPressure = 0.8f
}
when (me.action) {
MotionEvent.ACTION_DOWN -> {
run {
// 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)
} else if (mDragStartPos.x >= rightRect.left && mCurrentIndex < mPageProvider!!.pageCount) {
mDragStartPos.x = rightRect.right
if (!mAllowLastPageCurl && mCurrentIndex >= mPageProvider!!.pageCount - 1) {
return false
}
startCurl(CURL_RIGHT)
}// Otherwise check pointer is on right page's side.
} else if (mViewMode == SHOW_ONE_PAGE) {
val 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!!.pageCount) {
mDragStartPos.x = rightRect.right
if (!mAllowLastPageCurl && mCurrentIndex >= mPageProvider!!.pageCount - 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
}
}
updateCurlPos(mPointerPos)
}
MotionEvent.ACTION_MOVE -> {
updateCurlPos(mPointerPos)
}
MotionEvent.ACTION_CANCEL, 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()
}
}
}
return true
}
/**
* Allow the last page to curl.
*/
fun setAllowLastPageCurl(allowLastPageCurl: Boolean) {
mAllowLastPageCurl = allowLastPageCurl
}
/**
* Sets mPageCurl curl position.
*/
private fun setCurlPos(curlPos: PointF, curlDir: PointF, radius: Double) {
// First reposition curl so that page doesn't 'rip off' from book.
if (mCurlState == CURL_RIGHT || mCurlState == CURL_LEFT && mViewMode == SHOW_ONE_PAGE) {
val 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 != 0f) {
val diffX = curlPos.x - pageRect.left
val 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) {
val 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 != 0f) {
val diffX = curlPos.x - pageRect.right
val 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.
val dist = sqrt((curlDir.x * curlDir.x + curlDir.y * curlDir.y).toDouble())
if (dist != 0.0) {
curlDir.x /= dist.toFloat()
curlDir.y /= dist.toFloat()
mPageCurl.curl(curlPos, curlDir, radius)
} else {
mPageCurl.reset()
}
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.
*/
fun setEnableTouchPressure(enableTouchPressure: Boolean) {
mEnableTouchPressure = enableTouchPressure
}
/**
* 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.
*/
fun setRenderLeftPage(renderLeftPage: Boolean) {
mRenderLeftPage = renderLeftPage
}
/**
* Sets SizeChangedObserver for this View. Call back method is called from
* this View's onSizeChanged method.
*/
fun setSizeChangedObserver(observer: SizeChangedObserver) {
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.
*/
fun setViewMode(viewMode: Int) {
when (viewMode) {
SHOW_ONE_PAGE -> {
mViewMode = viewMode
mPageLeft.setFlipTexture(true)
mRenderer.setViewMode(CurlRenderer.SHOW_ONE_PAGE)
}
SHOW_TWO_PAGES -> {
mViewMode = viewMode
mPageLeft.setFlipTexture(false)
mRenderer.setViewMode(CurlRenderer.SHOW_TWO_PAGES)
}
}
}
/**
* Switches meshes and loads new bitmaps if available. Updated to support 2
* pages in landscape
*/
private fun startCurl(page: Int) {
when (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.
CURL_RIGHT -> {
// Remove meshes from renderer.
mRenderer.removeCurlMesh(mPageLeft)
mRenderer.removeCurlMesh(mPageRight)
mRenderer.removeCurlMesh(mPageCurl)
// We are curling right page.
val 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!!.pageCount - 1) {
updatePage(mPageRight.texturePage, 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
}
// 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.
CURL_LEFT -> {
// Remove meshes from renderer.
mRenderer.removeCurlMesh(mPageLeft)
mRenderer.removeCurlMesh(mPageRight)
mRenderer.removeCurlMesh(mPageCurl)
// We are curling left page.
val curl = mPageLeft
mPageLeft = mPageCurl
mPageCurl = curl
if (mCurrentIndex > 1) {
updatePage(mPageLeft.texturePage, 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!!.pageCount) {
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
}
}
}
/**
* Updates curl position.
*/
private fun updateCurlPos(pointerPos: PointerPosition) {
// Default curl radius.
var radius = (mRenderer.getPageRect(CURL_RIGHT)!!.width() / 3).toDouble()
// 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 *= max(1f - pointerPos.mPressure, 0f).toDouble()
// 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
val dist =
sqrt((mCurlDir.x * mCurlDir.x + mCurlDir.y * mCurlDir.y).toDouble()).toFloat()
// Adjust curl radius so that if page is dragged far enough on
// opposite side, radius gets closer to zero.
val pageWidth = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!
.width()
var curlLen = radius * Math.PI
if (dist > pageWidth * 2 - curlLen) {
curlLen = max(pageWidth * 2 - dist, 0f).toDouble()
radius = curlLen / Math.PI
}
// Actual curl position calculation.
if (dist >= curlLen) {
val translate = (dist - curlLen) / 2
if (mViewMode == SHOW_TWO_PAGES) {
mCurlPos.x -= (mCurlDir.x * translate / dist).toFloat()
} else {
val pageLeftX = mRenderer
.getPageRect(CurlRenderer.PAGE_RIGHT)!!.left
radius = max(
min((mCurlPos.x - pageLeftX).toDouble(), radius),
0.0
)
}
mCurlPos.y -= (mCurlDir.y * translate / dist).toFloat()
} else {
val angle = Math.PI * sqrt(dist / curlLen)
val translate = radius * sin(angle)
mCurlPos.x += (mCurlDir.x * translate / dist).toFloat()
mCurlPos.y += (mCurlDir.y * translate / dist).toFloat()
}
} else if (mCurlState == CURL_LEFT) {
// Adjust radius regarding how close to page edge we are.
val pageLeftX = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!.left
radius = max(min((mCurlPos.x - pageLeftX).toDouble(), radius), 0.0)
val pageRightX = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!.right
mCurlPos.x -= min((pageRightX - mCurlPos.x).toDouble(), radius).toFloat()
mCurlDir.x = mCurlPos.x + mDragStartPos.x
mCurlDir.y = mCurlPos.y - mDragStartPos.y
}// Otherwise we'll let curl follow pointer position.
setCurlPos(mCurlPos, mCurlDir, radius)
}
/**
* Updates given CurlPage via PageProvider for page located at index.
*/
private fun updatePage(page: CurlPage, index: Int) {
// 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.
*/
fun updatePages() {
if (mPageProvider == null || mPageBitmapWidth <= 0
|| mPageBitmapHeight <= 0
) {
return
}
// Remove meshes from renderer.
mRenderer.removeCurlMesh(mPageLeft)
mRenderer.removeCurlMesh(mPageRight)
mRenderer.removeCurlMesh(mPageCurl)
var leftIdx = mCurrentIndex - 1
var rightIdx = mCurrentIndex
var curlIdx = -1
if (mCurlState == CURL_LEFT) {
curlIdx = leftIdx
--leftIdx
} else if (mCurlState == CURL_RIGHT) {
curlIdx = rightIdx
++rightIdx
}
if (rightIdx >= 0 && rightIdx < mPageProvider!!.pageCount) {
updatePage(mPageRight.texturePage, rightIdx)
mPageRight.setFlipTexture(false)
mPageRight.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)!!)
mPageRight.reset()
mRenderer.addCurlMesh(mPageRight)
}
if (leftIdx >= 0 && leftIdx < mPageProvider!!.pageCount) {
updatePage(mPageLeft.texturePage, leftIdx)
mPageLeft.setFlipTexture(true)
mPageLeft.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)!!)
mPageLeft.reset()
if (mRenderLeftPage) {
mRenderer.addCurlMesh(mPageLeft)
}
}
if (curlIdx >= 0 && curlIdx < mPageProvider!!.pageCount) {
updatePage(mPageCurl.texturePage, 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.
*/
interface PageProvider {
/**
* Return number of pages available.
*/
val pageCount: Int
/**
* 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>
* <br></br>
* Index is a number between 0 and getBitmapCount() - 1.
*/
fun updatePage(page: CurlPage, width: Int, height: Int, index: Int)
}
/**
* Simple holder for pointer position.
*/
private inner class PointerPosition {
internal var mPos = PointF()
internal var mPressure: Float = 0.toFloat()
}
/**
* Observer interface for handling CurlView size changes.
*/
interface SizeChangedObserver {
/**
* Called once CurlView size changes.
*/
fun onSizeChanged(width: Int, height: Int)
}
interface CallBack {
fun pageChange(change: Int)
}
companion object {
// Curl state. We are flipping none, left or right page.
private const val CURL_LEFT = 1
private const val CURL_NONE = 0
private const val CURL_RIGHT = 2
// Constants for mAnimationTargetEvent.
private const val SET_CURL_TO_LEFT = 1
private const val SET_CURL_TO_RIGHT = 2
// Shows one page at the center of view.
const val SHOW_ONE_PAGE = 1
// Shows two pages side by side.
const val SHOW_TWO_PAGES = 2
}
}

@ -69,7 +69,7 @@ abstract class PageDelegate(protected val pageView: PageView) {
var isRunning = false
var isStarted = false
protected fun setStartPoint(x: Float, y: Float, invalidate: Boolean = true) {
open fun setStartPoint(x: Float, y: Float, invalidate: Boolean = true) {
startX = x
startY = y
@ -78,7 +78,7 @@ abstract class PageDelegate(protected val pageView: PageView) {
}
}
protected fun setTouchPoint(x: Float, y: Float, invalidate: Boolean = true) {
open fun setTouchPoint(x: Float, y: Float, invalidate: Boolean = true) {
touchX = x
touchY = y

@ -1,126 +1,526 @@
package io.legado.app.ui.book.read.page.delegate
import android.graphics.Canvas
import android.view.MotionEvent
import android.graphics.*
import android.graphics.drawable.GradientDrawable
import android.os.Build
import io.legado.app.ui.book.read.page.PageView
import io.legado.app.ui.book.read.page.curl.CurlPage
import io.legado.app.ui.book.read.page.curl.CurlView
import io.legado.app.utils.screenshot
import kotlin.math.abs
import kotlin.math.*
class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView),
CurlView.CallBack {
class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) {
var curlView: CurlView? = null
private var mCornerX = 1 // 拖拽点对应的页脚
private var mCornerY = 1
private val mPath0: Path = Path()
private val mPath1: Path = Path()
// 贝塞尔曲线起始点
private val mBezierStart1 = PointF()
// 贝塞尔曲线控制点
private val mBezierControl1 = PointF()
// 贝塞尔曲线顶点
private val mBezierVertex1 = PointF()
// 贝塞尔曲线结束点
private var mBezierEnd1 = PointF()
// 另一条贝塞尔曲线
private val mBezierStart2 = PointF()
private val mBezierControl2 = PointF()
private val mBezierVertex2 = PointF()
private var mBezierEnd2 = PointF()
private var mMiddleX = 0f
private var mMiddleY = 0f
private var mDegrees = 0f
private var mTouchToCornerDis = 0f
private var mColorMatrixFilter: ColorMatrixColorFilter? = null
private val mMatrix: Matrix = Matrix()
private val mMatrixArray = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 1.0f)
// 是否属于右上左下
private var mIsRT_LB = false
private var mMaxLength = 0f
// 背面颜色组
private var mBackShadowColors: IntArray? = null
// 前面颜色组
private var mFrontShadowColors: IntArray? = null
// 有阴影的GradientDrawable
private var mBackShadowDrawableLR: GradientDrawable? = null
private var mBackShadowDrawableRL: GradientDrawable? = null
private var mFolderShadowDrawableLR: GradientDrawable? = null
private var mFolderShadowDrawableRL: GradientDrawable? = null
private var mFrontShadowDrawableHBT: GradientDrawable? = null
private var mFrontShadowDrawableHTB: GradientDrawable? = null
private var mFrontShadowDrawableVLR: GradientDrawable? = null
private var mFrontShadowDrawableVRL: GradientDrawable? = null
private val mPaint: Paint = Paint()
init {
pageView.curlView ?: let {
curlView = CurlView(pageView.context)
pageView.curlView = curlView
pageView.addView(curlView)
curlView?.mPageProvider = PageProvider()
curlView?.setSizeChangedObserver(SizeChangedObserver())
curlView?.callBack = this
}
mMaxLength = hypot(pageView.width.toDouble(), pageView.height.toDouble()).toFloat()
mPaint.style = Paint.Style.FILL
//设置颜色数组
createDrawable()
val cm = ColorMatrix()
val array = floatArrayOf(
1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f,
0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f
)
cm.set(array)
mColorMatrixFilter = ColorMatrixColorFilter(cm)
}
override fun onTouch(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
curlView?.currentIndex = 1
}
override fun setStartPoint(x: Float, y: Float, invalidate: Boolean) {
super.setStartPoint(x, y, invalidate)
calcCornerXY(x, y)
}
override fun setTouchPoint(x: Float, y: Float, invalidate: Boolean) {
super.setTouchPoint(x, y, invalidate)
//触摸y中间位置吧y变成屏幕高度
//触摸y中间位置吧y变成屏幕高度
if (startY > pageView.height / 3.0
&& startY < pageView.height * 2 / 3.0
|| direction == Direction.PREV
) {
touchY = pageView.height.toFloat()
}
if (startY > pageView.height / 3.0
&& startY < pageView.height / 2.0
&& direction == Direction.NEXT
) {
touchY = 1f
}
curlView?.dispatchTouchEvent(event)
return super.onTouch(event)
}
override fun onScrollStart() {
}
val distanceX: Float
when (direction) {
Direction.NEXT -> distanceX =
if (isCancel) {
var dis = viewWidth - startX + touchX
if (dis > viewWidth) {
dis = viewWidth.toFloat()
}
viewWidth - dis
} else {
-(touchX + (viewWidth - startX))
}
else -> distanceX =
if (isCancel) {
-(touchX - startX)
} else {
viewWidth - (touchX - startX)
}
}
override fun onDraw(canvas: Canvas) {
startScroll(touchX.toInt(), 0, distanceX.toInt(), 0)
}
override fun onScrollStop() {
curPage?.x = 0.toFloat()
if (!isCancel) {
pageView.fillPage(direction)
}
}
override fun onScroll(
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (!isMoved) {
val event = e1.toAction(MotionEvent.ACTION_UP)
curPage?.dispatchTouchEvent(event)
event.recycle()
if (abs(distanceX) > abs(distanceY)) {
if (distanceX < 0) {
//如果上一页不存在
if (!hasPrev()) {
noNext = true
return true
}
//上一页截图
bitmap = prevPage?.screenshot()
} else {
//如果不存在表示没有下一页了
if (!hasNext()) {
noNext = true
return true
}
//下一页截图
bitmap = nextPage?.screenshot()
}
isMoved = true
override fun onDraw(canvas: Canvas) {
bitmap?.let {
if (direction === Direction.NEXT) {
calcPoints()
drawCurrentPageArea(canvas, it, mPath0!!) //绘制翻页时的正面页
// drawNextPageAreaAndShadow(canvas, it)
drawCurrentPageShadow(canvas)
drawCurrentBackArea(canvas, it)
} else {
calcPoints()
drawCurrentPageArea(canvas, it, mPath0!!)
// drawNextPageAreaAndShadow(canvas, it)
drawCurrentPageShadow(canvas)
drawCurrentBackArea(canvas, it)
}
}
if (isMoved) {
curlView?.canDraw = true
isCancel = if (pageView.isScrollDelegate) {
if (direction == Direction.NEXT) distanceY < 0 else distanceY > 0
}
/**
* 创建阴影的GradientDrawable
*/
private fun createDrawable() {
val color = intArrayOf(0x333333, -0x4fcccccd)
mFolderShadowDrawableRL = GradientDrawable(
GradientDrawable.Orientation.RIGHT_LEFT, color
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mFolderShadowDrawableLR = GradientDrawable(
GradientDrawable.Orientation.LEFT_RIGHT, color
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mBackShadowColors = intArrayOf(-0xeeeeef, 0x111111)
mBackShadowDrawableRL = GradientDrawable(
GradientDrawable.Orientation.RIGHT_LEFT, mBackShadowColors
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mBackShadowDrawableLR = GradientDrawable(
GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mFrontShadowColors = intArrayOf(-0x7feeeeef, 0x111111)
mFrontShadowDrawableVLR = GradientDrawable(
GradientDrawable.Orientation.LEFT_RIGHT, mFrontShadowColors
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mFrontShadowDrawableVRL = GradientDrawable(
GradientDrawable.Orientation.RIGHT_LEFT, mFrontShadowColors
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mFrontShadowDrawableHTB = GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM, mFrontShadowColors
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
mFrontShadowDrawableHBT = GradientDrawable(
GradientDrawable.Orientation.BOTTOM_TOP, mFrontShadowColors
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT }
}
/**
* 绘制翻起页背面
*/
private fun drawCurrentBackArea(
canvas: Canvas,
bitmap: Bitmap
) {
val i = (mBezierStart1.x + mBezierControl1.x).toInt() / 2
val f1 = abs(i - mBezierControl1.x)
val i1 = (mBezierStart2.y + mBezierControl2.y).toInt() / 2
val f2 = abs(i1 - mBezierControl2.y)
val f3 = min(f1, f2)
mPath1.reset()
mPath1.moveTo(mBezierVertex2.x, mBezierVertex2.y)
mPath1.lineTo(mBezierVertex1.x, mBezierVertex1.y)
mPath1.lineTo(mBezierEnd1.x, mBezierEnd1.y)
mPath1.lineTo(touchX, touchY)
mPath1.lineTo(mBezierEnd2.x, mBezierEnd2.y)
mPath1.close()
val mFolderShadowDrawable: GradientDrawable
val left: Int
val right: Int
if (mIsRT_LB) {
left = (mBezierStart1.x - 1).toInt()
right = (mBezierStart1.x + f3 + 1).toInt()
mFolderShadowDrawable = mFolderShadowDrawableLR!!
} else {
left = (mBezierStart1.x - f3 - 1).toInt()
right = (mBezierStart1.x + 1).toInt()
mFolderShadowDrawable = mFolderShadowDrawableRL!!
}
canvas.save()
try {
canvas.clipPath(mPath0)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipPath(mPath1)
} else {
if (direction == Direction.NEXT) distanceX < 0 else distanceX > 0
canvas.clipPath(mPath1, Region.Op.INTERSECT)
}
isRunning = true
//设置触摸点
setTouchPoint(e2.x, e2.y)
} catch (ignored: Exception) {
}
return isMoved
mPaint!!.colorFilter = mColorMatrixFilter
val dis = hypot(
mCornerX - mBezierControl1.x.toDouble(),
mBezierControl2.y - mCornerY.toDouble()
).toFloat()
val f8 = (mCornerX - mBezierControl1.x) / dis
val f9 = (mBezierControl2.y - mCornerY) / dis
mMatrixArray[0] = 1 - 2 * f9 * f9
mMatrixArray[1] = 2 * f8 * f9
mMatrixArray[3] = mMatrixArray[1]
mMatrixArray[4] = 1 - 2 * f8 * f8
mMatrix.reset()
mMatrix.setValues(mMatrixArray)
mMatrix.preTranslate(-mBezierControl1.x, -mBezierControl1.y)
mMatrix.postTranslate(mBezierControl1.x, mBezierControl1.y)
canvas.drawBitmap(bitmap, mMatrix, mPaint)
mPaint.colorFilter = null
canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y)
mFolderShadowDrawable.setBounds(
left, mBezierStart1.y.toInt(), right,
(mBezierStart1.y + mMaxLength).toInt()
)
mFolderShadowDrawable.draw(canvas)
canvas.restore()
}
override fun onPageUp() {
curlView?.updatePages()
curlView?.requestRender()
/**
* 绘制翻起页的阴影
*/
private fun drawCurrentPageShadow(canvas: Canvas) {
val degree: Double = if (mIsRT_LB) {
(Math.PI / 4 - atan2(mBezierControl1.y - touchX, touchY - mBezierControl1.x))
} else {
(Math.PI / 4 - atan2(touchY - mBezierControl1.y, touchX - mBezierControl1.x))
}
// 翻起页阴影顶点与touch点的距离
val d1 = 25.toFloat() * 1.414 * cos(degree)
val d2 = 25.toFloat() * 1.414 * sin(degree)
val x = (touchX + d1).toFloat()
val y: Float
y = if (mIsRT_LB) {
(touchY + d2).toFloat()
} else {
(touchY - d2).toFloat()
}
mPath1.reset()
mPath1.moveTo(x, y)
mPath1.lineTo(touchX, touchY)
mPath1.lineTo(mBezierControl1.x, mBezierControl1.y)
mPath1.lineTo(mBezierStart1.x, mBezierStart1.y)
mPath1.close()
canvas.save()
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipOutPath(mPath0)
} else {
canvas.clipPath(mPath0, Region.Op.XOR)
}
canvas.clipPath(mPath1, Region.Op.INTERSECT)
} catch (ignored: java.lang.Exception) {
}
var leftX: Int
var rightX: Int
var mCurrentPageShadow: GradientDrawable
if (mIsRT_LB) {
leftX = mBezierControl1.x.toInt()
rightX = mBezierControl1.x.toInt() + 25
mCurrentPageShadow = mFrontShadowDrawableVLR!!
} else {
leftX = (mBezierControl1.x - 25).toInt()
rightX = mBezierControl1.x.toInt() + 1
mCurrentPageShadow = mFrontShadowDrawableVRL!!
}
var rotateDegrees: Float =
Math.toDegrees(atan2(touchX - mBezierControl1.x, mBezierControl1.y - touchY).toDouble())
.toFloat()
canvas.rotate(rotateDegrees, mBezierControl1.x, mBezierControl1.y)
mCurrentPageShadow.setBounds(
leftX,
(mBezierControl1.y - mMaxLength).toInt(), rightX,
mBezierControl1.y.toInt()
)
mCurrentPageShadow.draw(canvas)
canvas.restore()
mPath1.reset()
mPath1.moveTo(x, y)
mPath1.lineTo(touchX, touchY)
mPath1.lineTo(mBezierControl2.x, mBezierControl2.y)
mPath1.lineTo(mBezierStart2.x, mBezierStart2.y)
mPath1.close()
canvas.save()
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipOutPath(mPath0)
} else {
canvas.clipPath(mPath0, Region.Op.XOR)
}
canvas.clipPath(mPath1)
} catch (ignored: java.lang.Exception) {
}
if (mIsRT_LB) {
leftX = mBezierControl2.y.toInt()
rightX = (mBezierControl2.y + 25).toInt()
mCurrentPageShadow = mFrontShadowDrawableHTB!!
} else {
leftX = (mBezierControl2.y - 25).toInt()
rightX = (mBezierControl2.y + 1).toInt()
mCurrentPageShadow = mFrontShadowDrawableHBT!!
}
rotateDegrees = Math.toDegrees(
atan2(mBezierControl2.y - touchY, mBezierControl2.x - touchX).toDouble()
).toFloat()
canvas.rotate(rotateDegrees, mBezierControl2.x, mBezierControl2.y)
val temp: Float =
if (mBezierControl2.y < 0) mBezierControl2.y - pageView.height else mBezierControl2.y
val hmg = hypot(mBezierControl2.x.toDouble(), temp.toDouble()).toInt()
if (hmg > mMaxLength) mCurrentPageShadow
.setBounds(
(mBezierControl2.x - 25).toInt() - hmg, leftX,
(mBezierControl2.x + mMaxLength).toInt() - hmg,
rightX
) else mCurrentPageShadow.setBounds(
(mBezierControl2.x - mMaxLength).toInt(), leftX,
mBezierControl2.x.toInt(), rightX
)
mCurrentPageShadow.draw(canvas)
canvas.restore()
}
override fun pageChange(change: Int) {
pageView.post {
if (change > 0) {
pageView.moveToNextPage()
private fun drawNextPageAreaAndShadow(
canvas: Canvas,
bitmap: Bitmap
) {
mPath1.reset()
mPath1.moveTo(mBezierStart1.x, mBezierStart1.y)
mPath1.lineTo(mBezierVertex1.x, mBezierVertex1.y)
mPath1.lineTo(mBezierVertex2.x, mBezierVertex2.y)
mPath1.lineTo(mBezierStart2.x, mBezierStart2.y)
mPath1.lineTo(mCornerX.toFloat(), mCornerY.toFloat())
mPath1.close()
mDegrees = Math.toDegrees(
atan2(
(mBezierControl1.x - mCornerX).toDouble(),
mBezierControl2.y - mCornerY.toDouble()
)
).toFloat()
val leftX: Int
val rightY: Int
val mBackShadowDrawable: GradientDrawable
if (mIsRT_LB) { //左下及右上
leftX = mBezierStart1.x.toInt()
rightY = (mBezierStart1.x + mTouchToCornerDis / 4).toInt()
mBackShadowDrawable = mBackShadowDrawableLR!!
} else {
leftX = (mBezierStart1.x - mTouchToCornerDis / 4).toInt()
rightY = mBezierStart1.x.toInt()
mBackShadowDrawable = mBackShadowDrawableRL!!
}
canvas.save()
try {
canvas.clipPath(mPath0)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipPath(mPath1)
} else {
pageView.moveToPrevPage()
canvas.clipPath(mPath1, Region.Op.INTERSECT)
}
//canvas.clipPath(mPath1, Region.Op.INTERSECT);
} catch (ignored: java.lang.Exception) {
}
canvas.drawBitmap(bitmap, 0f, 0f, null)
canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y)
mBackShadowDrawable.setBounds(
leftX, mBezierStart1.y.toInt(), rightY,
(mMaxLength + mBezierStart1.y).toInt()
) //左上及右下角的xy坐标值,构成一个矩形
mBackShadowDrawable.draw(canvas)
canvas.restore()
}
private inner class PageProvider : CurlView.PageProvider {
private fun drawCurrentPageArea(
canvas: Canvas,
bitmap: Bitmap,
path: Path
) {
mPath0.reset()
mPath0.moveTo(mBezierStart1.x, mBezierStart1.y)
mPath0.quadTo(
mBezierControl1.x, mBezierControl1.y, mBezierEnd1.x,
mBezierEnd1.y
)
mPath0.lineTo(touchX, touchY)
mPath0.lineTo(mBezierEnd2.x, mBezierEnd2.y)
mPath0.quadTo(
mBezierControl2.x, mBezierControl2.y, mBezierStart2.x,
mBezierStart2.y
)
mPath0.lineTo(mCornerX.toFloat(), mCornerY.toFloat())
mPath0.close()
canvas.save()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipOutPath(path)
} else {
canvas.clipPath(path, Region.Op.XOR)
}
canvas.drawBitmap(bitmap, 0f, 0f, null)
try {
canvas.restore()
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
}
override val pageCount: Int
get() = 3
/**
* 计算拖拽点对应的拖拽脚
*/
private fun calcCornerXY(x: Float, y: Float) {
if (x <= pageView.width / 2.0) {
mCornerX = 0
} else {
mCornerX = pageView.width
}
if (y <= pageView.height / 2.0) {
mCornerY = 0
} else {
mCornerY = pageView.height
}
mIsRT_LB = (mCornerX == 0 && mCornerY == pageView.height
|| mCornerX == pageView.width && mCornerY == 0)
}
override fun updatePage(page: CurlPage, width: Int, height: Int, index: Int) {
when (index) {
0 -> page.setTexture(prevPage?.screenshot(), CurlPage.SIDE_BOTH)
1 -> page.setTexture(curPage?.screenshot(), CurlPage.SIDE_BOTH)
2 -> page.setTexture(nextPage?.screenshot(), CurlPage.SIDE_BOTH)
private fun calcPoints() {
mMiddleX = (touchX + mCornerX) / 2
mMiddleY = (touchY + mCornerY) / 2
mBezierControl1.x =
mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX)
mBezierControl1.y = mCornerY.toFloat()
mBezierControl2.x = mCornerX.toFloat()
val f4 = mCornerY - mMiddleY
if (f4 == 0f) {
mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f
} else {
mBezierControl2.y =
mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY)
}
mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2
mBezierStart1.y = mCornerY.toFloat()
// 当mBezierStart1.x < 0或者mBezierStart1.x > 480时
// 如果继续翻页,会出现BUG故在此限制
if (touchX > 0 && touchX < pageView.width) {
if (mBezierStart1.x < 0 || mBezierStart1.x > pageView.width) {
if (mBezierStart1.x < 0) mBezierStart1.x = pageView.width - mBezierStart1.x
val f1: Float = abs(mCornerX - touchX)
val f2: Float = pageView.width * f1 / mBezierStart1.x
touchX = abs(mCornerX - f2)
val f3: Float = abs(mCornerX - touchX) * abs(mCornerY - touchX) / f1
touchX = abs(mCornerY - f3)
mMiddleX = (touchX + mCornerX) / 2
mMiddleY = (touchY + mCornerY) / 2
mBezierControl1.x =
mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX)
mBezierControl1.y = mCornerY.toFloat()
mBezierControl2.x = mCornerX.toFloat()
val f5 = mCornerY - mMiddleY
if (f5 == 0f) {
mBezierControl2.y =
mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f
} else {
mBezierControl2.y =
mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY)
}
mBezierStart1.x = (mBezierControl1.x
- (mCornerX - mBezierControl1.x) / 2)
}
}
mBezierStart2.x = mCornerX.toFloat()
mBezierStart2.y = mBezierControl2.y - (mCornerY - mBezierControl2.y) / 2
mTouchToCornerDis = hypot(touchX - mCornerX, touchY - mCornerY)
mBezierEnd1 = getCross(
PointF(touchX, touchY), mBezierControl1, mBezierStart1,
mBezierStart2
)
mBezierEnd2 = getCross(
PointF(touchX, touchY), mBezierControl2, mBezierStart1,
mBezierStart2
)
mBezierVertex1.x = (mBezierStart1.x + 2 * mBezierControl1.x + mBezierEnd1.x) / 4
mBezierVertex1.y = (2 * mBezierControl1.y + mBezierStart1.y + mBezierEnd1.y) / 4
mBezierVertex2.x = (mBezierStart2.x + 2 * mBezierControl2.x + mBezierEnd2.x) / 4
mBezierVertex2.y = (2 * mBezierControl2.y + mBezierStart2.y + mBezierEnd2.y) / 4
}
// 定义书籍尺寸的变化监听器
private inner class SizeChangedObserver : CurlView.SizeChangedObserver {
override fun onSizeChanged(width: Int, height: Int) {
curlView?.setViewMode(CurlView.SHOW_ONE_PAGE)
}
/**
* 求解直线P1P2和直线P3P4的交点坐标
*/
private fun getCross(P1: PointF, P2: PointF, P3: PointF, P4: PointF): PointF {
val crossP = PointF()
// 二元函数通式: y=ax+b
val a1 = (P2.y - P1.y) / (P2.x - P1.x)
val b1 = (P1.x * P2.y - P2.x * P1.y) / (P1.x - P2.x)
val a2 = (P4.y - P3.y) / (P4.x - P3.x)
val b2 = (P3.x * P4.y - P4.x * P3.y) / (P3.x - P4.x)
crossP.x = (b2 - b1) / (a1 - a2)
crossP.y = a1 * crossP.x + b1
return crossP
}
}
Loading…
Cancel
Save