commit
77aa0bb317
@ -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 |
package io.legado.app.ui.book.read.page.delegate |
||||||
|
|
||||||
import android.graphics.Canvas |
import android.graphics.* |
||||||
import android.view.MotionEvent |
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.PageView |
||||||
import io.legado.app.ui.book.read.page.curl.CurlPage |
import kotlin.math.* |
||||||
import io.legado.app.ui.book.read.page.curl.CurlView |
|
||||||
import io.legado.app.utils.screenshot |
|
||||||
import kotlin.math.abs |
|
||||||
|
|
||||||
class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView), |
class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { |
||||||
CurlView.CallBack { |
|
||||||
|
|
||||||
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 { |
init { |
||||||
pageView.curlView ?: let { |
mMaxLength = hypot(pageView.width.toDouble(), pageView.height.toDouble()).toFloat() |
||||||
curlView = CurlView(pageView.context) |
mPaint.style = Paint.Style.FILL |
||||||
pageView.curlView = curlView |
//设置颜色数组 |
||||||
pageView.addView(curlView) |
createDrawable() |
||||||
curlView?.mPageProvider = PageProvider() |
val cm = ColorMatrix() |
||||||
curlView?.setSizeChangedObserver(SizeChangedObserver()) |
val array = floatArrayOf( |
||||||
curlView?.callBack = this |
1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, |
||||||
} |
0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f |
||||||
|
) |
||||||
|
cm.set(array) |
||||||
|
mColorMatrixFilter = ColorMatrixColorFilter(cm) |
||||||
} |
} |
||||||
|
|
||||||
override fun onTouch(event: MotionEvent): Boolean { |
override fun setStartPoint(x: Float, y: Float, invalidate: Boolean) { |
||||||
when (event.action) { |
super.setStartPoint(x, y, invalidate) |
||||||
MotionEvent.ACTION_DOWN -> { |
calcCornerXY(x, y) |
||||||
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() { |
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() { |
override fun onScrollStop() { |
||||||
|
curPage?.x = 0.toFloat() |
||||||
|
if (!isCancel) { |
||||||
|
pageView.fillPage(direction) |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
override fun onScroll( |
override fun onDraw(canvas: Canvas) { |
||||||
e1: MotionEvent, |
bitmap?.let { |
||||||
e2: MotionEvent, |
if (direction === Direction.NEXT) { |
||||||
distanceX: Float, |
calcPoints() |
||||||
distanceY: Float |
drawCurrentPageArea(canvas, it, mPath0) //绘制翻页时的正面页 |
||||||
): Boolean { |
drawNextPageAreaAndShadow(canvas, it) |
||||||
if (!isMoved) { |
drawCurrentPageShadow(canvas) |
||||||
val event = e1.toAction(MotionEvent.ACTION_UP) |
drawCurrentBackArea(canvas, it) |
||||||
curPage?.dispatchTouchEvent(event) |
} else { |
||||||
event.recycle() |
calcPoints() |
||||||
if (abs(distanceX) > abs(distanceY)) { |
drawCurrentPageArea(canvas, it, mPath0) |
||||||
if (distanceX < 0) { |
drawNextPageAreaAndShadow(canvas, it) |
||||||
//如果上一页不存在 |
drawCurrentPageShadow(canvas) |
||||||
if (!hasPrev()) { |
drawCurrentBackArea(canvas, it) |
||||||
noNext = true |
|
||||||
return true |
|
||||||
} |
|
||||||
//上一页截图 |
|
||||||
bitmap = prevPage?.screenshot() |
|
||||||
} else { |
|
||||||
//如果不存在表示没有下一页了 |
|
||||||
if (!hasNext()) { |
|
||||||
noNext = true |
|
||||||
return true |
|
||||||
} |
|
||||||
//下一页截图 |
|
||||||
bitmap = nextPage?.screenshot() |
|
||||||
} |
|
||||||
isMoved = true |
|
||||||
} |
} |
||||||
} |
} |
||||||
if (isMoved) { |
} |
||||||
curlView?.canDraw = true |
|
||||||
isCancel = if (pageView.isScrollDelegate) { |
/** |
||||||
if (direction == Direction.NEXT) distanceY < 0 else distanceY > 0 |
* 创建阴影的GradientDrawable |
||||||
|
*/ |
||||||
|
private fun createDrawable() { |
||||||
|
val color = intArrayOf(0x333333, -0x4fcccccd) |
||||||
|
mFolderShadowDrawableRL = GradientDrawable( |
||||||
|
GradientDrawable.Orientation.RIGHT_LEFT, color |
||||||
|
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } |
||||||
|
mFolderShadowDrawableLR = GradientDrawable( |
||||||
|
GradientDrawable.Orientation.LEFT_RIGHT, color |
||||||
|
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } |
||||||
|
mBackShadowColors = intArrayOf(-0xeeeeef, 0x111111) |
||||||
|
mBackShadowDrawableRL = GradientDrawable( |
||||||
|
GradientDrawable.Orientation.RIGHT_LEFT, mBackShadowColors |
||||||
|
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } |
||||||
|
mBackShadowDrawableLR = GradientDrawable( |
||||||
|
GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors |
||||||
|
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } |
||||||
|
mFrontShadowColors = intArrayOf(-0x7feeeeef, 0x111111) |
||||||
|
mFrontShadowDrawableVLR = GradientDrawable( |
||||||
|
GradientDrawable.Orientation.LEFT_RIGHT, mFrontShadowColors |
||||||
|
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } |
||||||
|
mFrontShadowDrawableVRL = GradientDrawable( |
||||||
|
GradientDrawable.Orientation.RIGHT_LEFT, mFrontShadowColors |
||||||
|
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } |
||||||
|
mFrontShadowDrawableHTB = GradientDrawable( |
||||||
|
GradientDrawable.Orientation.TOP_BOTTOM, mFrontShadowColors |
||||||
|
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } |
||||||
|
mFrontShadowDrawableHBT = GradientDrawable( |
||||||
|
GradientDrawable.Orientation.BOTTOM_TOP, mFrontShadowColors |
||||||
|
).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 绘制翻起页背面 |
||||||
|
*/ |
||||||
|
private fun drawCurrentBackArea( |
||||||
|
canvas: Canvas, |
||||||
|
bitmap: Bitmap |
||||||
|
) { |
||||||
|
val i = (mBezierStart1.x + mBezierControl1.x).toInt() / 2 |
||||||
|
val f1 = abs(i - mBezierControl1.x) |
||||||
|
val i1 = (mBezierStart2.y + mBezierControl2.y).toInt() / 2 |
||||||
|
val f2 = abs(i1 - mBezierControl2.y) |
||||||
|
val f3 = min(f1, f2) |
||||||
|
mPath1.reset() |
||||||
|
mPath1.moveTo(mBezierVertex2.x, mBezierVertex2.y) |
||||||
|
mPath1.lineTo(mBezierVertex1.x, mBezierVertex1.y) |
||||||
|
mPath1.lineTo(mBezierEnd1.x, mBezierEnd1.y) |
||||||
|
mPath1.lineTo(touchX, touchY) |
||||||
|
mPath1.lineTo(mBezierEnd2.x, mBezierEnd2.y) |
||||||
|
mPath1.close() |
||||||
|
val mFolderShadowDrawable: GradientDrawable |
||||||
|
val left: Int |
||||||
|
val right: Int |
||||||
|
if (mIsRT_LB) { |
||||||
|
left = (mBezierStart1.x - 1).toInt() |
||||||
|
right = (mBezierStart1.x + f3 + 1).toInt() |
||||||
|
mFolderShadowDrawable = mFolderShadowDrawableLR!! |
||||||
|
} else { |
||||||
|
left = (mBezierStart1.x - f3 - 1).toInt() |
||||||
|
right = (mBezierStart1.x + 1).toInt() |
||||||
|
mFolderShadowDrawable = mFolderShadowDrawableRL!! |
||||||
|
} |
||||||
|
canvas.save() |
||||||
|
try { |
||||||
|
canvas.clipPath(mPath0) |
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
||||||
|
canvas.clipPath(mPath1) |
||||||
} else { |
} else { |
||||||
if (direction == Direction.NEXT) distanceX < 0 else distanceX > 0 |
canvas.clipPath(mPath1, Region.Op.INTERSECT) |
||||||
} |
} |
||||||
isRunning = true |
} catch (ignored: Exception) { |
||||||
//设置触摸点 |
|
||||||
setTouchPoint(e2.x, e2.y) |
|
||||||
} |
} |
||||||
return isMoved |
mPaint.colorFilter = mColorMatrixFilter |
||||||
|
val dis = hypot( |
||||||
|
mCornerX - mBezierControl1.x.toDouble(), |
||||||
|
mBezierControl2.y - mCornerY.toDouble() |
||||||
|
).toFloat() |
||||||
|
val f8 = (mCornerX - mBezierControl1.x) / dis |
||||||
|
val f9 = (mBezierControl2.y - mCornerY) / dis |
||||||
|
mMatrixArray[0] = 1 - 2 * f9 * f9 |
||||||
|
mMatrixArray[1] = 2 * f8 * f9 |
||||||
|
mMatrixArray[3] = mMatrixArray[1] |
||||||
|
mMatrixArray[4] = 1 - 2 * f8 * f8 |
||||||
|
mMatrix.reset() |
||||||
|
mMatrix.setValues(mMatrixArray) |
||||||
|
mMatrix.preTranslate(-mBezierControl1.x, -mBezierControl1.y) |
||||||
|
mMatrix.postTranslate(mBezierControl1.x, mBezierControl1.y) |
||||||
|
canvas.drawBitmap(bitmap, mMatrix, mPaint) |
||||||
|
mPaint.colorFilter = null |
||||||
|
canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y) |
||||||
|
mFolderShadowDrawable.setBounds( |
||||||
|
left, mBezierStart1.y.toInt(), right, |
||||||
|
(mBezierStart1.y + mMaxLength).toInt() |
||||||
|
) |
||||||
|
mFolderShadowDrawable.draw(canvas) |
||||||
|
canvas.restore() |
||||||
} |
} |
||||||
|
|
||||||
override fun onPageUp() { |
/** |
||||||
curlView?.updatePages() |
* 绘制翻起页的阴影 |
||||||
curlView?.requestRender() |
*/ |
||||||
|
private fun drawCurrentPageShadow(canvas: Canvas) { |
||||||
|
val degree: Double = if (mIsRT_LB) { |
||||||
|
(Math.PI / 4 - atan2(mBezierControl1.y - touchX, touchY - mBezierControl1.x)) |
||||||
|
} else { |
||||||
|
(Math.PI / 4 - atan2(touchY - mBezierControl1.y, touchX - mBezierControl1.x)) |
||||||
|
} |
||||||
|
// 翻起页阴影顶点与touch点的距离 |
||||||
|
val d1 = 25.toFloat() * 1.414 * cos(degree) |
||||||
|
val d2 = 25.toFloat() * 1.414 * sin(degree) |
||||||
|
val x = (touchX + d1).toFloat() |
||||||
|
val y: Float |
||||||
|
y = if (mIsRT_LB) { |
||||||
|
(touchY + d2).toFloat() |
||||||
|
} else { |
||||||
|
(touchY - d2).toFloat() |
||||||
|
} |
||||||
|
mPath1.reset() |
||||||
|
mPath1.moveTo(x, y) |
||||||
|
mPath1.lineTo(touchX, touchY) |
||||||
|
mPath1.lineTo(mBezierControl1.x, mBezierControl1.y) |
||||||
|
mPath1.lineTo(mBezierStart1.x, mBezierStart1.y) |
||||||
|
mPath1.close() |
||||||
|
canvas.save() |
||||||
|
try { |
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
||||||
|
canvas.clipOutPath(mPath0) |
||||||
|
} else { |
||||||
|
canvas.clipPath(mPath0, Region.Op.XOR) |
||||||
|
} |
||||||
|
canvas.clipPath(mPath1, Region.Op.INTERSECT) |
||||||
|
} catch (ignored: java.lang.Exception) { |
||||||
|
} |
||||||
|
var leftX: Int |
||||||
|
var rightX: Int |
||||||
|
var mCurrentPageShadow: GradientDrawable |
||||||
|
if (mIsRT_LB) { |
||||||
|
leftX = mBezierControl1.x.toInt() |
||||||
|
rightX = mBezierControl1.x.toInt() + 25 |
||||||
|
mCurrentPageShadow = mFrontShadowDrawableVLR!! |
||||||
|
} else { |
||||||
|
leftX = (mBezierControl1.x - 25).toInt() |
||||||
|
rightX = mBezierControl1.x.toInt() + 1 |
||||||
|
mCurrentPageShadow = mFrontShadowDrawableVRL!! |
||||||
|
} |
||||||
|
var rotateDegrees: Float = |
||||||
|
Math.toDegrees(atan2(touchX - mBezierControl1.x, mBezierControl1.y - touchY).toDouble()) |
||||||
|
.toFloat() |
||||||
|
canvas.rotate(rotateDegrees, mBezierControl1.x, mBezierControl1.y) |
||||||
|
mCurrentPageShadow.setBounds( |
||||||
|
leftX, |
||||||
|
(mBezierControl1.y - mMaxLength).toInt(), rightX, |
||||||
|
mBezierControl1.y.toInt() |
||||||
|
) |
||||||
|
mCurrentPageShadow.draw(canvas) |
||||||
|
canvas.restore() |
||||||
|
mPath1.reset() |
||||||
|
mPath1.moveTo(x, y) |
||||||
|
mPath1.lineTo(touchX, touchY) |
||||||
|
mPath1.lineTo(mBezierControl2.x, mBezierControl2.y) |
||||||
|
mPath1.lineTo(mBezierStart2.x, mBezierStart2.y) |
||||||
|
mPath1.close() |
||||||
|
canvas.save() |
||||||
|
try { |
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
||||||
|
canvas.clipOutPath(mPath0) |
||||||
|
} else { |
||||||
|
canvas.clipPath(mPath0, Region.Op.XOR) |
||||||
|
} |
||||||
|
canvas.clipPath(mPath1) |
||||||
|
} catch (ignored: java.lang.Exception) { |
||||||
|
} |
||||||
|
if (mIsRT_LB) { |
||||||
|
leftX = mBezierControl2.y.toInt() |
||||||
|
rightX = (mBezierControl2.y + 25).toInt() |
||||||
|
mCurrentPageShadow = mFrontShadowDrawableHTB!! |
||||||
|
} else { |
||||||
|
leftX = (mBezierControl2.y - 25).toInt() |
||||||
|
rightX = (mBezierControl2.y + 1).toInt() |
||||||
|
mCurrentPageShadow = mFrontShadowDrawableHBT!! |
||||||
|
} |
||||||
|
rotateDegrees = Math.toDegrees( |
||||||
|
atan2(mBezierControl2.y - touchY, mBezierControl2.x - touchX).toDouble() |
||||||
|
).toFloat() |
||||||
|
canvas.rotate(rotateDegrees, mBezierControl2.x, mBezierControl2.y) |
||||||
|
val temp: Float = |
||||||
|
if (mBezierControl2.y < 0) mBezierControl2.y - pageView.height else mBezierControl2.y |
||||||
|
val hmg = hypot(mBezierControl2.x.toDouble(), temp.toDouble()).toInt() |
||||||
|
if (hmg > mMaxLength) mCurrentPageShadow |
||||||
|
.setBounds( |
||||||
|
(mBezierControl2.x - 25).toInt() - hmg, leftX, |
||||||
|
(mBezierControl2.x + mMaxLength).toInt() - hmg, |
||||||
|
rightX |
||||||
|
) else mCurrentPageShadow.setBounds( |
||||||
|
(mBezierControl2.x - mMaxLength).toInt(), leftX, |
||||||
|
mBezierControl2.x.toInt(), rightX |
||||||
|
) |
||||||
|
mCurrentPageShadow.draw(canvas) |
||||||
|
canvas.restore() |
||||||
} |
} |
||||||
|
|
||||||
override fun pageChange(change: Int) { |
private fun drawNextPageAreaAndShadow( |
||||||
pageView.post { |
canvas: Canvas, |
||||||
if (change > 0) { |
bitmap: Bitmap |
||||||
pageView.moveToNextPage() |
) { |
||||||
|
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 { |
} else { |
||||||
pageView.moveToPrevPage() |
canvas.clipPath(mPath1, Region.Op.INTERSECT) |
||||||
} |
} |
||||||
|
//canvas.clipPath(mPath1, Region.Op.INTERSECT); |
||||||
|
} catch (ignored: java.lang.Exception) { |
||||||
} |
} |
||||||
|
canvas.drawBitmap(bitmap, 0f, 0f, null) |
||||||
|
canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y) |
||||||
|
mBackShadowDrawable.setBounds( |
||||||
|
leftX, mBezierStart1.y.toInt(), rightY, |
||||||
|
(mMaxLength + mBezierStart1.y).toInt() |
||||||
|
) //左上及右下角的xy坐标值,构成一个矩形 |
||||||
|
mBackShadowDrawable.draw(canvas) |
||||||
|
canvas.restore() |
||||||
} |
} |
||||||
|
|
||||||
private inner class PageProvider : CurlView.PageProvider { |
private fun drawCurrentPageArea( |
||||||
|
canvas: Canvas, |
||||||
|
bitmap: Bitmap, |
||||||
|
path: Path |
||||||
|
) { |
||||||
|
mPath0.reset() |
||||||
|
mPath0.moveTo(mBezierStart1.x, mBezierStart1.y) |
||||||
|
mPath0.quadTo( |
||||||
|
mBezierControl1.x, mBezierControl1.y, mBezierEnd1.x, |
||||||
|
mBezierEnd1.y |
||||||
|
) |
||||||
|
mPath0.lineTo(touchX, touchY) |
||||||
|
mPath0.lineTo(mBezierEnd2.x, mBezierEnd2.y) |
||||||
|
mPath0.quadTo( |
||||||
|
mBezierControl2.x, mBezierControl2.y, mBezierStart2.x, |
||||||
|
mBezierStart2.y |
||||||
|
) |
||||||
|
mPath0.lineTo(mCornerX.toFloat(), mCornerY.toFloat()) |
||||||
|
mPath0.close() |
||||||
|
canvas.save() |
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
||||||
|
canvas.clipOutPath(path) |
||||||
|
} else { |
||||||
|
canvas.clipPath(path, Region.Op.XOR) |
||||||
|
} |
||||||
|
canvas.drawBitmap(bitmap, 0f, 0f, null) |
||||||
|
try { |
||||||
|
canvas.restore() |
||||||
|
} catch (e: java.lang.Exception) { |
||||||
|
e.printStackTrace() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
override val pageCount: Int |
/** |
||||||
get() = 3 |
* 计算拖拽点对应的拖拽脚 |
||||||
|
*/ |
||||||
|
private fun calcCornerXY(x: Float, y: Float) { |
||||||
|
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) |
||||||
|
} |
||||||
|
|
||||||
override fun updatePage(page: CurlPage, width: Int, height: Int, index: Int) { |
private fun calcPoints() { |
||||||
when (index) { |
mMiddleX = (touchX + mCornerX) / 2 |
||||||
0 -> page.setTexture(prevPage?.screenshot(), CurlPage.SIDE_BOTH) |
mMiddleY = (touchY + mCornerY) / 2 |
||||||
1 -> page.setTexture(curPage?.screenshot(), CurlPage.SIDE_BOTH) |
mBezierControl1.x = |
||||||
2 -> page.setTexture(nextPage?.screenshot(), CurlPage.SIDE_BOTH) |
mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX) |
||||||
|
mBezierControl1.y = mCornerY.toFloat() |
||||||
|
mBezierControl2.x = mCornerX.toFloat() |
||||||
|
val f4 = mCornerY - mMiddleY |
||||||
|
if (f4 == 0f) { |
||||||
|
mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f |
||||||
|
} else { |
||||||
|
mBezierControl2.y = |
||||||
|
mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY) |
||||||
|
} |
||||||
|
mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2 |
||||||
|
mBezierStart1.y = mCornerY.toFloat() |
||||||
|
// 当mBezierStart1.x < 0或者mBezierStart1.x > 480时 |
||||||
|
// 如果继续翻页,会出现BUG故在此限制 |
||||||
|
if (touchX > 0 && touchX < pageView.width) { |
||||||
|
if (mBezierStart1.x < 0 || mBezierStart1.x > pageView.width) { |
||||||
|
if (mBezierStart1.x < 0) mBezierStart1.x = pageView.width - mBezierStart1.x |
||||||
|
val f1: Float = abs(mCornerX - touchX) |
||||||
|
val f2: Float = pageView.width * f1 / mBezierStart1.x |
||||||
|
touchX = abs(mCornerX - f2) |
||||||
|
val f3: Float = abs(mCornerX - touchX) * abs(mCornerY - touchX) / f1 |
||||||
|
touchX = abs(mCornerY - f3) |
||||||
|
mMiddleX = (touchX + mCornerX) / 2 |
||||||
|
mMiddleY = (touchY + mCornerY) / 2 |
||||||
|
mBezierControl1.x = |
||||||
|
mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX) |
||||||
|
mBezierControl1.y = mCornerY.toFloat() |
||||||
|
mBezierControl2.x = mCornerX.toFloat() |
||||||
|
val f5 = mCornerY - mMiddleY |
||||||
|
if (f5 == 0f) { |
||||||
|
mBezierControl2.y = |
||||||
|
mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f |
||||||
|
} else { |
||||||
|
mBezierControl2.y = |
||||||
|
mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY) |
||||||
|
} |
||||||
|
mBezierStart1.x = (mBezierControl1.x |
||||||
|
- (mCornerX - mBezierControl1.x) / 2) |
||||||
} |
} |
||||||
} |
} |
||||||
|
mBezierStart2.x = mCornerX.toFloat() |
||||||
|
mBezierStart2.y = mBezierControl2.y - (mCornerY - mBezierControl2.y) / 2 |
||||||
|
mTouchToCornerDis = hypot(touchX - mCornerX, touchY - mCornerY) |
||||||
|
mBezierEnd1 = getCross( |
||||||
|
PointF(touchX, touchY), mBezierControl1, mBezierStart1, |
||||||
|
mBezierStart2 |
||||||
|
) |
||||||
|
mBezierEnd2 = getCross( |
||||||
|
PointF(touchX, touchY), mBezierControl2, mBezierStart1, |
||||||
|
mBezierStart2 |
||||||
|
) |
||||||
|
mBezierVertex1.x = (mBezierStart1.x + 2 * mBezierControl1.x + mBezierEnd1.x) / 4 |
||||||
|
mBezierVertex1.y = (2 * mBezierControl1.y + mBezierStart1.y + mBezierEnd1.y) / 4 |
||||||
|
mBezierVertex2.x = (mBezierStart2.x + 2 * mBezierControl2.x + mBezierEnd2.x) / 4 |
||||||
|
mBezierVertex2.y = (2 * mBezierControl2.y + mBezierStart2.y + mBezierEnd2.y) / 4 |
||||||
} |
} |
||||||
|
|
||||||
// 定义书籍尺寸的变化监听器 |
/** |
||||||
private inner class SizeChangedObserver : CurlView.SizeChangedObserver { |
* 求解直线P1P2和直线P3P4的交点坐标 |
||||||
override fun onSizeChanged(width: Int, height: Int) { |
*/ |
||||||
curlView?.setViewMode(CurlView.SHOW_ONE_PAGE) |
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 |
package io.legado.app.ui.download |
||||||
|
|
||||||
import android.content.Context |
import android.content.Context |
||||||
|
import android.view.View |
||||||
import io.legado.app.R |
import io.legado.app.R |
||||||
import io.legado.app.base.adapter.ItemViewHolder |
import io.legado.app.base.adapter.ItemViewHolder |
||||||
import io.legado.app.base.adapter.SimpleRecyclerAdapter |
import io.legado.app.base.adapter.SimpleRecyclerAdapter |
||||||
import io.legado.app.data.entities.Book |
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) : |
class DownloadAdapter(context: Context) : |
||||||
SimpleRecyclerAdapter<Book>(context, R.layout.item_download) { |
SimpleRecyclerAdapter<Book>(context, R.layout.item_download) { |
||||||
|
|
||||||
|
|
||||||
override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) { |
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 |
package io.legado.app.ui.widget.font |
||||||
|
|
||||||
import android.annotation.SuppressLint |
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.os.Environment |
||||||
|
import android.util.DisplayMetrics |
||||||
import android.view.LayoutInflater |
import android.view.LayoutInflater |
||||||
|
import android.view.MenuItem |
||||||
import android.view.View |
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 androidx.recyclerview.widget.LinearLayoutManager |
||||||
|
import io.legado.app.App |
||||||
import io.legado.app.R |
import io.legado.app.R |
||||||
import io.legado.app.lib.dialogs.AlertBuilder |
import io.legado.app.constant.PreferKey |
||||||
import io.legado.app.lib.dialogs.alert |
import io.legado.app.help.FileHelp |
||||||
import io.legado.app.utils.applyTint |
import io.legado.app.help.permission.Permissions |
||||||
import io.legado.app.utils.invisible |
import io.legado.app.help.permission.PermissionsCompat |
||||||
import io.legado.app.utils.visible |
import io.legado.app.utils.DocumentUtils |
||||||
import kotlinx.android.synthetic.main.dialog_font_select.view.* |
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 java.io.File |
||||||
|
import kotlin.coroutines.CoroutineContext |
||||||
|
|
||||||
class FontSelectDialog(context: Context) : FontAdapter.CallBack { |
class FontSelectDialog : DialogFragment(), |
||||||
|
Toolbar.OnMenuItemClickListener, |
||||||
private val defaultFolder = |
CoroutineScope, |
||||||
Environment.getExternalStorageDirectory().absolutePath + File.separator + "Fonts" |
FontAdapter.CallBack { |
||||||
|
lateinit var job: Job |
||||||
|
private val fontFolderRequestCode = 35485 |
||||||
private lateinit var adapter: FontAdapter |
private lateinit var adapter: FontAdapter |
||||||
private var builder: AlertBuilder<AlertDialog> |
private val fontFolder = |
||||||
private var dialog: AlertDialog? = null |
App.INSTANCE.filesDir.absolutePath + File.separator + "Fonts" + File.separator |
||||||
@SuppressLint("InflateParams") |
override val coroutineContext: CoroutineContext |
||||||
private var view: View = LayoutInflater.from(context).inflate(R.layout.dialog_font_select, null) |
get() = job + Main |
||||||
var curPath: String? = null |
|
||||||
var fontFolder: String? = null |
|
||||||
var defaultFont: (() -> Unit)? = null |
|
||||||
var selectFile: ((path: String) -> Unit)? = null |
|
||||||
|
|
||||||
init { |
override fun onStart() { |
||||||
builder = context.alert(title = context.getString(R.string.select_font)) { |
super.onStart() |
||||||
customView = view |
val dm = DisplayMetrics() |
||||||
positiveButton(R.string.default_font) { defaultFont?.invoke() } |
activity?.windowManager?.defaultDisplay?.getMetrics(dm) |
||||||
negativeButton(R.string.cancel) |
dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) |
||||||
} |
|
||||||
initData() |
|
||||||
} |
} |
||||||
|
|
||||||
fun show() { |
override fun onCreateView( |
||||||
dialog = builder.show().applyTint() |
inflater: LayoutInflater, |
||||||
|
container: ViewGroup?, |
||||||
|
savedInstanceState: Bundle? |
||||||
|
): View? { |
||||||
|
job = Job() |
||||||
|
return inflater.inflate(R.layout.dialog_font_select, container) |
||||||
} |
} |
||||||
|
|
||||||
private fun initData() = with(view) { |
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
||||||
adapter = FontAdapter(context, this@FontSelectDialog) |
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.layoutManager = LinearLayoutManager(context) |
||||||
recycler_view.adapter = adapter |
recycler_view.adapter = adapter |
||||||
val files = getFontFiles() |
|
||||||
if (files.isNullOrEmpty()) { |
val fontPath = getPrefString(PreferKey.fontFolder) |
||||||
tv_no_data.visible() |
if (fontPath.isNullOrEmpty()) { |
||||||
|
openFolder() |
||||||
} else { |
} else { |
||||||
tv_no_data.invisible() |
val uri = Uri.parse(fontPath) |
||||||
adapter.setItems(files.toList()) |
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") |
@SuppressLint("DefaultLocale") |
||||||
private fun getFontFiles(): Array<File>? { |
private fun getFontFiles(uri: Uri) { |
||||||
val path = if (fontFolder.isNullOrEmpty()) { |
launch(IO) { |
||||||
defaultFolder |
DocumentFile.fromTreeUri(requireContext(), uri)?.listFiles()?.forEach { file -> |
||||||
} else fontFolder |
if (file.name?.toLowerCase()?.matches(".*\\.[ot]tf".toRegex()) == true) { |
||||||
return try { |
DocumentUtils.readBytes(App.INSTANCE, file.uri)?.let { |
||||||
val file = File(path) |
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 getFontFilesOld() { |
||||||
|
try { |
||||||
|
val file = |
||||||
|
File(Environment.getExternalStorageDirectory().absolutePath + File.separator + "Fonts") |
||||||
file.listFiles { pathName -> |
file.listFiles { pathName -> |
||||||
pathName.name.toLowerCase().matches(".*\\.[ot]tf".toRegex()) |
pathName.name.toLowerCase().matches(".*\\.[ot]tf".toRegex()) |
||||||
|
}?.let { |
||||||
|
adapter.setItems(it.toList()) |
||||||
} |
} |
||||||
} catch (e: Exception) { |
} catch (e: Exception) { |
||||||
null |
toast(e.localizedMessage ?: "") |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
override fun onClick(file: File) { |
override fun onClick(file: File) { |
||||||
file.absolutePath.let { |
file.absolutePath.let { |
||||||
if (it != curPath) { |
val pf = parentFragment |
||||||
selectFile?.invoke(it) |
if (pf is CallBack) { |
||||||
dialog?.dismiss() |
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 { |
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"?> |
<?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:orientation="vertical" |
||||||
android:layout_width="match_parent" |
android:layout_width="match_parent" |
||||||
android:layout_height="wrap_content" |
android:layout_height="wrap_content" |
||||||
android:padding="16dp"> |
android:padding="16dp"> |
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView |
<androidx.appcompat.widget.Toolbar |
||||||
android:id="@+id/recycler_view" |
android:id="@+id/tool_bar" |
||||||
android:layout_width="match_parent" |
android:layout_width="match_parent" |
||||||
android:layout_height="wrap_content" /> |
android:layout_height="wrap_content" /> |
||||||
|
|
||||||
<TextView |
<androidx.recyclerview.widget.RecyclerView |
||||||
android:id="@+id/tv_no_data" |
android:id="@+id/recycler_view" |
||||||
android:layout_width="match_parent" |
android:layout_width="match_parent" |
||||||
android:layout_height="wrap_content" |
android:layout_height="match_parent" /> |
||||||
android:padding="16dp" |
|
||||||
android:text="@string/fonts_folder" /> |
|
||||||
|
|
||||||
</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