commit
85090349d5
@ -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 |
||||
} |
||||
|
||||
} |
@ -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 setStartPoint(x: Float, y: Float, invalidate: Boolean) { |
||||
super.setStartPoint(x, y, invalidate) |
||||
calcCornerXY(x, y) |
||||
} |
||||
|
||||
override fun onTouch(event: MotionEvent): Boolean { |
||||
when (event.action) { |
||||
MotionEvent.ACTION_DOWN -> { |
||||
curlView?.currentIndex = 1 |
||||
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() |
||||
|
||||
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 { |
||||
//如果不存在表示没有下一页了 |
||||
if (!hasNext()) { |
||||
noNext = true |
||||
return true |
||||
calcPoints() |
||||
drawCurrentPageArea(canvas, it, mPath0) |
||||
drawNextPageAreaAndShadow(canvas, it) |
||||
drawCurrentPageShadow(canvas) |
||||
drawCurrentBackArea(canvas, it) |
||||
} |
||||
//下一页截图 |
||||
bitmap = nextPage?.screenshot() |
||||
} |
||||
isMoved = true |
||||
} |
||||
|
||||
/** |
||||
* 创建阴影的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 } |
||||
} |
||||
if (isMoved) { |
||||
curlView?.canDraw = true |
||||
isCancel = if (pageView.isScrollDelegate) { |
||||
if (direction == Direction.NEXT) distanceY < 0 else distanceY > 0 |
||||
|
||||
/** |
||||
* 绘制翻起页背面 |
||||
*/ |
||||
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 { |
||||
if (direction == Direction.NEXT) distanceX < 0 else distanceX > 0 |
||||
left = (mBezierStart1.x - f3 - 1).toInt() |
||||
right = (mBezierStart1.x + 1).toInt() |
||||
mFolderShadowDrawable = mFolderShadowDrawableRL!! |
||||
} |
||||
isRunning = true |
||||
//设置触摸点 |
||||
setTouchPoint(e2.x, e2.y) |
||||
canvas.save() |
||||
try { |
||||
canvas.clipPath(mPath0) |
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
||||
canvas.clipPath(mPath1) |
||||
} else { |
||||
canvas.clipPath(mPath1, Region.Op.INTERSECT) |
||||
} |
||||
return isMoved |
||||
} catch (ignored: Exception) { |
||||
} |
||||
|
||||
override fun onPageUp() { |
||||
curlView?.updatePages() |
||||
curlView?.requestRender() |
||||
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 pageChange(change: Int) { |
||||
pageView.post { |
||||
if (change > 0) { |
||||
pageView.moveToNextPage() |
||||
/** |
||||
* 绘制翻起页的阴影 |
||||
*/ |
||||
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 { |
||||
pageView.moveToPrevPage() |
||||
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() |
||||
} |
||||
|
||||
private inner class PageProvider : CurlView.PageProvider { |
||||
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 { |
||||
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() |
||||
} |
||||
|
||||
override val pageCount: Int |
||||
get() = 3 |
||||
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 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 calcCornerXY(x: Float, y: Float) { |
||||
mCornerX = if (x <= pageView.width / 2.0) { |
||||
0 |
||||
} else { |
||||
pageView.width |
||||
} |
||||
mCornerY = if (y <= pageView.height / 2.0) { |
||||
0 |
||||
} else { |
||||
pageView.height |
||||
} |
||||
mIsRT_LB = (mCornerX == 0 && mCornerY == pageView.height |
||||
|| mCornerX == pageView.width && mCornerY == 0) |
||||
} |
||||
|
||||
// 定义书籍尺寸的变化监听器 |
||||
private inner class SizeChangedObserver : CurlView.SizeChangedObserver { |
||||
override fun onSizeChanged(width: Int, height: Int) { |
||||
curlView?.setViewMode(CurlView.SHOW_ONE_PAGE) |
||||
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 |
||||
} |
||||
|
||||
/** |
||||
* 求解直线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 |
||||
} |
||||
} |
@ -1,18 +1,36 @@ |
||||
package io.legado.app.ui.download |
||||
|
||||
import android.content.Context |
||||
import android.view.View |
||||
import io.legado.app.R |
||||
import io.legado.app.base.adapter.ItemViewHolder |
||||
import io.legado.app.base.adapter.SimpleRecyclerAdapter |
||||
import io.legado.app.data.entities.Book |
||||
import io.legado.app.help.BookHelp |
||||
import kotlinx.android.synthetic.main.item_download.view.* |
||||
|
||||
|
||||
class DownloadAdapter(context: Context) : |
||||
SimpleRecyclerAdapter<Book>(context, R.layout.item_download) { |
||||
|
||||
|
||||
override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) { |
||||
with(holder.itemView) { |
||||
if (payloads.isEmpty()) { |
||||
tv_name.text = item.name |
||||
tv_author.text = item.author |
||||
upDownloadCount(this, item) |
||||
} else { |
||||
upDownloadCount(this, item) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun upDownloadCount(view: View, book: Book) { |
||||
view.tv_download.text = context.getString( |
||||
R.string.download_count, |
||||
BookHelp.getChapterCount(book), |
||||
book.totalChapterNum |
||||
) |
||||
} |
||||
|
||||
} |
@ -1,86 +1,221 @@ |
||||
package io.legado.app.ui.widget.font |
||||
|
||||
import android.annotation.SuppressLint |
||||
import android.content.Context |
||||
import android.app.Activity.RESULT_OK |
||||
import android.content.Intent |
||||
import android.net.Uri |
||||
import android.os.Bundle |
||||
import android.os.Environment |
||||
import android.util.DisplayMetrics |
||||
import android.view.LayoutInflater |
||||
import android.view.MenuItem |
||||
import android.view.View |
||||
import androidx.appcompat.app.AlertDialog |
||||
import android.view.ViewGroup |
||||
import androidx.appcompat.widget.Toolbar |
||||
import androidx.documentfile.provider.DocumentFile |
||||
import androidx.fragment.app.DialogFragment |
||||
import androidx.recyclerview.widget.LinearLayoutManager |
||||
import io.legado.app.App |
||||
import io.legado.app.R |
||||
import io.legado.app.lib.dialogs.AlertBuilder |
||||
import io.legado.app.lib.dialogs.alert |
||||
import io.legado.app.utils.applyTint |
||||
import io.legado.app.utils.invisible |
||||
import io.legado.app.utils.visible |
||||
import kotlinx.android.synthetic.main.dialog_font_select.view.* |
||||
import io.legado.app.constant.PreferKey |
||||
import io.legado.app.help.FileHelp |
||||
import io.legado.app.help.permission.Permissions |
||||
import io.legado.app.help.permission.PermissionsCompat |
||||
import io.legado.app.utils.DocumentUtils |
||||
import io.legado.app.utils.getPrefString |
||||
import io.legado.app.utils.putPrefString |
||||
import io.legado.app.utils.toast |
||||
import kotlinx.android.synthetic.main.dialog_font_select.* |
||||
import kotlinx.coroutines.CoroutineScope |
||||
import kotlinx.coroutines.Dispatchers.IO |
||||
import kotlinx.coroutines.Dispatchers.Main |
||||
import kotlinx.coroutines.Job |
||||
import kotlinx.coroutines.launch |
||||
import kotlinx.coroutines.withContext |
||||
import java.io.File |
||||
import kotlin.coroutines.CoroutineContext |
||||
|
||||
class FontSelectDialog(context: Context) : FontAdapter.CallBack { |
||||
|
||||
private val defaultFolder = |
||||
Environment.getExternalStorageDirectory().absolutePath + File.separator + "Fonts" |
||||
class FontSelectDialog : DialogFragment(), |
||||
Toolbar.OnMenuItemClickListener, |
||||
CoroutineScope, |
||||
FontAdapter.CallBack { |
||||
lateinit var job: Job |
||||
private val fontFolderRequestCode = 35485 |
||||
private lateinit var adapter: FontAdapter |
||||
private var builder: AlertBuilder<AlertDialog> |
||||
private var dialog: AlertDialog? = null |
||||
@SuppressLint("InflateParams") |
||||
private var view: View = LayoutInflater.from(context).inflate(R.layout.dialog_font_select, null) |
||||
var curPath: String? = null |
||||
var fontFolder: String? = null |
||||
var defaultFont: (() -> Unit)? = null |
||||
var selectFile: ((path: String) -> Unit)? = null |
||||
private val fontFolder = |
||||
App.INSTANCE.filesDir.absolutePath + File.separator + "Fonts" + File.separator |
||||
override val coroutineContext: CoroutineContext |
||||
get() = job + Main |
||||
|
||||
init { |
||||
builder = context.alert(title = context.getString(R.string.select_font)) { |
||||
customView = view |
||||
positiveButton(R.string.default_font) { defaultFont?.invoke() } |
||||
negativeButton(R.string.cancel) |
||||
} |
||||
initData() |
||||
override fun onStart() { |
||||
super.onStart() |
||||
val dm = DisplayMetrics() |
||||
activity?.windowManager?.defaultDisplay?.getMetrics(dm) |
||||
dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) |
||||
} |
||||
|
||||
fun show() { |
||||
dialog = builder.show().applyTint() |
||||
override fun onCreateView( |
||||
inflater: LayoutInflater, |
||||
container: ViewGroup?, |
||||
savedInstanceState: Bundle? |
||||
): View? { |
||||
job = Job() |
||||
return inflater.inflate(R.layout.dialog_font_select, container) |
||||
} |
||||
|
||||
private fun initData() = with(view) { |
||||
adapter = FontAdapter(context, this@FontSelectDialog) |
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
||||
super.onViewCreated(view, savedInstanceState) |
||||
tool_bar.setTitle(R.string.select_font) |
||||
tool_bar.inflateMenu(R.menu.font_select) |
||||
tool_bar.setOnMenuItemClickListener(this) |
||||
adapter = FontAdapter(requireContext(), this) |
||||
recycler_view.layoutManager = LinearLayoutManager(context) |
||||
recycler_view.adapter = adapter |
||||
val files = getFontFiles() |
||||
if (files.isNullOrEmpty()) { |
||||
tv_no_data.visible() |
||||
|
||||
val fontPath = getPrefString(PreferKey.fontFolder) |
||||
if (fontPath.isNullOrEmpty()) { |
||||
openFolder() |
||||
} else { |
||||
tv_no_data.invisible() |
||||
adapter.setItems(files.toList()) |
||||
val uri = Uri.parse(fontPath) |
||||
if (DocumentFile.fromTreeUri(requireContext(), uri)?.canRead() == true) { |
||||
getFontFiles(uri) |
||||
} else { |
||||
openFolder() |
||||
} |
||||
} |
||||
} |
||||
|
||||
override fun onMenuItemClick(item: MenuItem?): Boolean { |
||||
when (item?.itemId) { |
||||
R.id.menu_default -> { |
||||
val pf = parentFragment |
||||
if (pf is CallBack) { |
||||
if ("" != pf.curFontPath) { |
||||
pf.selectFile("") |
||||
} |
||||
} |
||||
val activity = activity |
||||
if (activity is CallBack) { |
||||
if ("" != activity.curFontPath) { |
||||
activity.selectFile("") |
||||
} |
||||
} |
||||
dismiss() |
||||
} |
||||
R.id.menu_other -> { |
||||
openFolder() |
||||
} |
||||
} |
||||
return true |
||||
} |
||||
|
||||
override fun onDestroy() { |
||||
super.onDestroy() |
||||
job.cancel() |
||||
} |
||||
|
||||
private fun openFolder() { |
||||
try { |
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) |
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) |
||||
startActivityForResult(intent, fontFolderRequestCode) |
||||
} catch (e: java.lang.Exception) { |
||||
PermissionsCompat.Builder(this) |
||||
.addPermissions(*Permissions.Group.STORAGE) |
||||
.rationale(R.string.tip_perm_request_storage) |
||||
.onGranted { getFontFilesOld() } |
||||
.request() |
||||
} |
||||
} |
||||
|
||||
@SuppressLint("DefaultLocale") |
||||
private fun getFontFiles(uri: Uri) { |
||||
launch(IO) { |
||||
DocumentFile.fromTreeUri(requireContext(), uri)?.listFiles()?.forEach { file -> |
||||
if (file.name?.toLowerCase()?.matches(".*\\.[ot]tf".toRegex()) == true) { |
||||
DocumentUtils.readBytes(App.INSTANCE, file.uri)?.let { |
||||
FileHelp.getFile(fontFolder + file.name).writeBytes(it) |
||||
} |
||||
} |
||||
} |
||||
try { |
||||
val file = File(fontFolder) |
||||
file.listFiles { pathName -> |
||||
pathName.name.toLowerCase().matches(".*\\.[ot]tf".toRegex()) |
||||
}?.let { |
||||
withContext(Main) { |
||||
adapter.setItems(it.toList()) |
||||
} |
||||
} |
||||
} catch (e: Exception) { |
||||
toast(e.localizedMessage ?: "") |
||||
} |
||||
} |
||||
} |
||||
|
||||
@SuppressLint("DefaultLocale") |
||||
private fun getFontFiles(): Array<File>? { |
||||
val path = if (fontFolder.isNullOrEmpty()) { |
||||
defaultFolder |
||||
} else fontFolder |
||||
return try { |
||||
val file = File(path) |
||||
private fun getFontFilesOld() { |
||||
try { |
||||
val file = |
||||
File(Environment.getExternalStorageDirectory().absolutePath + File.separator + "Fonts") |
||||
file.listFiles { pathName -> |
||||
pathName.name.toLowerCase().matches(".*\\.[ot]tf".toRegex()) |
||||
}?.let { |
||||
adapter.setItems(it.toList()) |
||||
} |
||||
} catch (e: Exception) { |
||||
null |
||||
toast(e.localizedMessage ?: "") |
||||
} |
||||
} |
||||
|
||||
override fun onClick(file: File) { |
||||
file.absolutePath.let { |
||||
if (it != curPath) { |
||||
selectFile?.invoke(it) |
||||
dialog?.dismiss() |
||||
val pf = parentFragment |
||||
if (pf is CallBack) { |
||||
if (it != pf.curFontPath) { |
||||
pf.selectFile(it) |
||||
} |
||||
} |
||||
val activity = activity |
||||
if (activity is CallBack) { |
||||
if (it != activity.curFontPath) { |
||||
activity.selectFile(it) |
||||
} |
||||
} |
||||
} |
||||
dialog?.dismiss() |
||||
} |
||||
|
||||
override fun curFilePath(): String { |
||||
return curPath ?: "" |
||||
val pf = parentFragment |
||||
if (pf is CallBack) { |
||||
return pf.curFontPath |
||||
} |
||||
val activity = activity |
||||
if (activity is CallBack) { |
||||
return activity.curFontPath |
||||
} |
||||
return "" |
||||
} |
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { |
||||
super.onActivityResult(requestCode, resultCode, data) |
||||
when (requestCode) { |
||||
fontFolderRequestCode -> if (resultCode == RESULT_OK) { |
||||
data?.data?.let { uri -> |
||||
putPrefString(PreferKey.fontFolder, uri.toString()) |
||||
context?.contentResolver?.takePersistableUriPermission( |
||||
uri, |
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION |
||||
) |
||||
getFontFiles(uri) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
interface CallBack { |
||||
fun selectFile(path: String) |
||||
val curFontPath: String |
||||
} |
||||
} |
@ -0,0 +1,52 @@ |
||||
package io.legado.app.utils |
||||
|
||||
import android.content.Context |
||||
import android.net.Uri |
||||
|
||||
object DocumentUtils { |
||||
|
||||
@JvmStatic |
||||
fun writeText(context: Context, data: String, fileUri: Uri): Boolean { |
||||
return writeBytes(context, data.toByteArray(), fileUri) |
||||
} |
||||
|
||||
@JvmStatic |
||||
fun writeBytes(context: Context, data: ByteArray, fileUri: Uri): Boolean { |
||||
try { |
||||
context.contentResolver.openOutputStream(fileUri)?.let { |
||||
it.write(data) |
||||
it.close() |
||||
return true |
||||
} |
||||
} catch (e: java.lang.Exception) { |
||||
e.printStackTrace() |
||||
} |
||||
return false |
||||
} |
||||
|
||||
@JvmStatic |
||||
fun readText(context: Context, uri: Uri): String? { |
||||
readBytes(context, uri)?.let { |
||||
return String(it) |
||||
} |
||||
return null |
||||
} |
||||
|
||||
@JvmStatic |
||||
fun readBytes(context: Context, uri: Uri): ByteArray? { |
||||
try { |
||||
context.contentResolver.openInputStream(uri)?.let { |
||||
val len: Int = it.available() |
||||
val buffer = ByteArray(len) |
||||
it.read(buffer) |
||||
it.close() |
||||
return buffer |
||||
} |
||||
} catch (e: Exception) { |
||||
e.printStackTrace() |
||||
} |
||||
return null |
||||
} |
||||
|
||||
|
||||
} |
Before Width: | Height: | Size: 3.1 KiB |
@ -0,0 +1,8 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<!--实现应用背景颜色渐变--> |
||||
<gradient |
||||
android:startColor="#17000000" |
||||
android:endColor="#00000000" |
||||
android:angle="90" /> |
||||
</shape> |
Before Width: | Height: | Size: 418 B |
@ -0,0 +1,8 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<!--实现应用背景颜色渐变--> |
||||
<gradient |
||||
android:startColor="#17FFFFFF" |
||||
android:endColor="#00FFFFFF" |
||||
android:angle="90" /> |
||||
</shape> |
Before Width: | Height: | Size: 340 B |
@ -0,0 +1,8 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<!--实现应用背景颜色渐变--> |
||||
<gradient |
||||
android:startColor="#17000000" |
||||
android:endColor="#00000000" |
||||
android:angle="270" /> |
||||
</shape> |
Before Width: | Height: | Size: 424 B |
@ -0,0 +1,8 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<!--实现应用背景颜色渐变--> |
||||
<gradient |
||||
android:startColor="#17FFFFFF" |
||||
android:endColor="#00FFFFFF" |
||||
android:angle="270" /> |
||||
</shape> |
@ -1,9 +0,0 @@ |
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:shape="rectangle"> |
||||
<gradient |
||||
android:angle="135" |
||||
android:centerColor="#009688" |
||||
android:endColor="#00695C" |
||||
android:startColor="#4DB6AC" |
||||
android:type="linear" /> |
||||
</shape> |
@ -1,20 +1,18 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:orientation="vertical" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content" |
||||
android:padding="16dp"> |
||||
|
||||
<androidx.recyclerview.widget.RecyclerView |
||||
android:id="@+id/recycler_view" |
||||
<androidx.appcompat.widget.Toolbar |
||||
android:id="@+id/tool_bar" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content" /> |
||||
|
||||
<TextView |
||||
android:id="@+id/tv_no_data" |
||||
<androidx.recyclerview.widget.RecyclerView |
||||
android:id="@+id/recycler_view" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content" |
||||
android:padding="16dp" |
||||
android:text="@string/fonts_folder" /> |
||||
android:layout_height="match_parent" /> |
||||
|
||||
</FrameLayout> |
||||
</LinearLayout> |
@ -0,0 +1,11 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:app="http://schemas.android.com/apk/res-auto"> |
||||
|
||||
<item |
||||
android:id="@+id/menu_download" |
||||
android:title="@string/action_download" |
||||
android:icon="@drawable/ic_play_24dp" |
||||
app:showAsAction="ifRoom" /> |
||||
|
||||
</menu> |
@ -0,0 +1,15 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:app="http://schemas.android.com/apk/res-auto"> |
||||
|
||||
<item |
||||
android:id="@+id/menu_default" |
||||
android:title="@string/default_font" |
||||
app:showAsAction="ifRoom" /> |
||||
|
||||
<item |
||||
android:id="@+id/menu_other" |
||||
android:title="@string/other_folder" |
||||
app:showAsAction="never" /> |
||||
|
||||
</menu> |
Loading…
Reference in new issue