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 | ||||
| 
 | ||||
| import android.graphics.Canvas | ||||
| import android.view.MotionEvent | ||||
| import android.graphics.* | ||||
| import android.graphics.drawable.GradientDrawable | ||||
| import android.os.Build | ||||
| import io.legado.app.ui.book.read.page.PageView | ||||
| import io.legado.app.ui.book.read.page.curl.CurlPage | ||||
| import io.legado.app.ui.book.read.page.curl.CurlView | ||||
| import io.legado.app.utils.screenshot | ||||
| import kotlin.math.abs | ||||
| import kotlin.math.* | ||||
| 
 | ||||
| class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView), | ||||
|     CurlView.CallBack { | ||||
| class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { | ||||
| 
 | ||||
|     var curlView: CurlView? = null | ||||
|     private var mCornerX = 1 // 拖拽点对应的页脚 | ||||
| 
 | ||||
|     private var mCornerY = 1 | ||||
|     private val mPath0: Path = Path() | ||||
|     private val mPath1: Path = Path() | ||||
|     // 贝塞尔曲线起始点 | ||||
|     private val mBezierStart1 = PointF() | ||||
|     // 贝塞尔曲线控制点 | ||||
|     private val mBezierControl1 = PointF() | ||||
|     // 贝塞尔曲线顶点 | ||||
|     private val mBezierVertex1 = PointF() | ||||
|     // 贝塞尔曲线结束点 | ||||
|     private var mBezierEnd1 = PointF() | ||||
| 
 | ||||
|     // 另一条贝塞尔曲线 | ||||
|     private val mBezierStart2 = PointF() | ||||
| 
 | ||||
|     private val mBezierControl2 = PointF() | ||||
|     private val mBezierVertex2 = PointF() | ||||
|     private var mBezierEnd2 = PointF() | ||||
| 
 | ||||
|     private var mMiddleX = 0f | ||||
|     private var mMiddleY = 0f | ||||
|     private var mDegrees = 0f | ||||
|     private var mTouchToCornerDis = 0f | ||||
|     private var mColorMatrixFilter: ColorMatrixColorFilter? = null | ||||
|     private val mMatrix: Matrix = Matrix() | ||||
|     private val mMatrixArray = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 1.0f) | ||||
| 
 | ||||
|     // 是否属于右上左下 | ||||
|     private var mIsRT_LB = false | ||||
|     private var mMaxLength = 0f | ||||
|     // 背面颜色组 | ||||
|     private var mBackShadowColors: IntArray? = null | ||||
|     // 前面颜色组 | ||||
|     private var mFrontShadowColors: IntArray? = null | ||||
|     // 有阴影的GradientDrawable | ||||
|     private var mBackShadowDrawableLR: GradientDrawable? = null | ||||
|     private var mBackShadowDrawableRL: GradientDrawable? = null | ||||
|     private var mFolderShadowDrawableLR: GradientDrawable? = null | ||||
|     private var mFolderShadowDrawableRL: GradientDrawable? = null | ||||
| 
 | ||||
|     private var mFrontShadowDrawableHBT: GradientDrawable? = null | ||||
|     private var mFrontShadowDrawableHTB: GradientDrawable? = null | ||||
|     private var mFrontShadowDrawableVLR: GradientDrawable? = null | ||||
|     private var mFrontShadowDrawableVRL: GradientDrawable? = null | ||||
| 
 | ||||
|     private val mPaint: Paint = Paint() | ||||
| 
 | ||||
|     init { | ||||
|         pageView.curlView ?: let { | ||||
|             curlView = CurlView(pageView.context) | ||||
|             pageView.curlView = curlView | ||||
|             pageView.addView(curlView) | ||||
|             curlView?.mPageProvider = PageProvider() | ||||
|             curlView?.setSizeChangedObserver(SizeChangedObserver()) | ||||
|             curlView?.callBack = this | ||||
|         } | ||||
|         mMaxLength = hypot(pageView.width.toDouble(), pageView.height.toDouble()).toFloat() | ||||
|         mPaint.style = Paint.Style.FILL | ||||
|         //设置颜色数组 | ||||
|         createDrawable() | ||||
|         val cm = ColorMatrix() | ||||
|         val array = floatArrayOf( | ||||
|             1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, | ||||
|             0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f | ||||
|         ) | ||||
|         cm.set(array) | ||||
|         mColorMatrixFilter = ColorMatrixColorFilter(cm) | ||||
|     } | ||||
| 
 | ||||
|     override fun onTouch(event: MotionEvent): Boolean { | ||||
|         when (event.action) { | ||||
|             MotionEvent.ACTION_DOWN -> { | ||||
|                 curlView?.currentIndex = 1 | ||||
|             } | ||||
|     override fun setStartPoint(x: Float, y: Float, invalidate: Boolean) { | ||||
|         super.setStartPoint(x, y, invalidate) | ||||
|         calcCornerXY(x, y) | ||||
|     } | ||||
| 
 | ||||
|     override fun setTouchPoint(x: Float, y: Float, invalidate: Boolean) { | ||||
|         super.setTouchPoint(x, y, invalidate) | ||||
|         //触摸y中间位置吧y变成屏幕高度 | ||||
|         //触摸y中间位置吧y变成屏幕高度 | ||||
|         if (startY > pageView.height / 3.0 | ||||
|             && startY < pageView.height * 2 / 3.0 | ||||
|             || direction == Direction.PREV | ||||
|         ) { | ||||
|             touchY = pageView.height.toFloat() | ||||
|         } | ||||
| 
 | ||||
|         if (startY > pageView.height / 3.0 | ||||
|             && startY < pageView.height / 2.0 | ||||
|             && direction == Direction.NEXT | ||||
|         ) { | ||||
|             touchY = 1f | ||||
|         } | ||||
|         curlView?.dispatchTouchEvent(event) | ||||
|         return super.onTouch(event) | ||||
|     } | ||||
| 
 | ||||
|     override fun onScrollStart() { | ||||
|     } | ||||
|         val distanceX: Float | ||||
|         when (direction) { | ||||
|             Direction.NEXT -> distanceX = | ||||
|                 if (isCancel) { | ||||
|                     var dis = viewWidth - startX + touchX | ||||
|                     if (dis > viewWidth) { | ||||
|                         dis = viewWidth.toFloat() | ||||
|                     } | ||||
|                     viewWidth - dis | ||||
|                 } else { | ||||
|                     -(touchX + (viewWidth - startX)) | ||||
|                 } | ||||
|             else -> distanceX = | ||||
|                 if (isCancel) { | ||||
|                     -(touchX - startX) | ||||
|                 } else { | ||||
|                     viewWidth - (touchX - startX) | ||||
|                 } | ||||
|         } | ||||
| 
 | ||||
|     override fun onDraw(canvas: Canvas) { | ||||
|         startScroll(touchX.toInt(), 0, distanceX.toInt(), 0) | ||||
|     } | ||||
| 
 | ||||
|     override fun onScrollStop() { | ||||
|         curPage?.x = 0.toFloat() | ||||
|         if (!isCancel) { | ||||
|             pageView.fillPage(direction) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onScroll( | ||||
|         e1: MotionEvent, | ||||
|         e2: MotionEvent, | ||||
|         distanceX: Float, | ||||
|         distanceY: Float | ||||
|     ): Boolean { | ||||
|         if (!isMoved) { | ||||
|             val event = e1.toAction(MotionEvent.ACTION_UP) | ||||
|             curPage?.dispatchTouchEvent(event) | ||||
|             event.recycle() | ||||
|             if (abs(distanceX) > abs(distanceY)) { | ||||
|                 if (distanceX < 0) { | ||||
|                     //如果上一页不存在 | ||||
|                     if (!hasPrev()) { | ||||
|                         noNext = true | ||||
|                         return true | ||||
|                     } | ||||
|                     //上一页截图 | ||||
|                     bitmap = prevPage?.screenshot() | ||||
|                 } else { | ||||
|                     //如果不存在表示没有下一页了 | ||||
|                     if (!hasNext()) { | ||||
|                         noNext = true | ||||
|                         return true | ||||
|                     } | ||||
|                     //下一页截图 | ||||
|                     bitmap = nextPage?.screenshot() | ||||
|                 } | ||||
|                 isMoved = true | ||||
|     override fun onDraw(canvas: Canvas) { | ||||
|         bitmap?.let { | ||||
|             if (direction === Direction.NEXT) { | ||||
|                 calcPoints() | ||||
|                 drawCurrentPageArea(canvas, it, mPath0) //绘制翻页时的正面页 | ||||
|                 drawNextPageAreaAndShadow(canvas, it) | ||||
|                 drawCurrentPageShadow(canvas) | ||||
|                 drawCurrentBackArea(canvas, it) | ||||
|             } else { | ||||
|                 calcPoints() | ||||
|                 drawCurrentPageArea(canvas, it, mPath0) | ||||
|                 drawNextPageAreaAndShadow(canvas, it) | ||||
|                 drawCurrentPageShadow(canvas) | ||||
|                 drawCurrentBackArea(canvas, it) | ||||
|             } | ||||
|         } | ||||
|         if (isMoved) { | ||||
|             curlView?.canDraw = true | ||||
|             isCancel = if (pageView.isScrollDelegate) { | ||||
|                 if (direction == Direction.NEXT) distanceY < 0 else distanceY > 0 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 创建阴影的GradientDrawable | ||||
|      */ | ||||
|     private fun createDrawable() { | ||||
|         val color = intArrayOf(0x333333, -0x4fcccccd) | ||||
|         mFolderShadowDrawableRL = GradientDrawable( | ||||
|             GradientDrawable.Orientation.RIGHT_LEFT, color | ||||
|         ).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } | ||||
|         mFolderShadowDrawableLR = GradientDrawable( | ||||
|             GradientDrawable.Orientation.LEFT_RIGHT, color | ||||
|         ).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } | ||||
|         mBackShadowColors = intArrayOf(-0xeeeeef, 0x111111) | ||||
|         mBackShadowDrawableRL = GradientDrawable( | ||||
|             GradientDrawable.Orientation.RIGHT_LEFT, mBackShadowColors | ||||
|         ).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } | ||||
|         mBackShadowDrawableLR = GradientDrawable( | ||||
|             GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors | ||||
|         ).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } | ||||
|         mFrontShadowColors = intArrayOf(-0x7feeeeef, 0x111111) | ||||
|         mFrontShadowDrawableVLR = GradientDrawable( | ||||
|             GradientDrawable.Orientation.LEFT_RIGHT, mFrontShadowColors | ||||
|         ).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } | ||||
|         mFrontShadowDrawableVRL = GradientDrawable( | ||||
|             GradientDrawable.Orientation.RIGHT_LEFT, mFrontShadowColors | ||||
|         ).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } | ||||
|         mFrontShadowDrawableHTB = GradientDrawable( | ||||
|             GradientDrawable.Orientation.TOP_BOTTOM, mFrontShadowColors | ||||
|         ).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } | ||||
|         mFrontShadowDrawableHBT = GradientDrawable( | ||||
|             GradientDrawable.Orientation.BOTTOM_TOP, mFrontShadowColors | ||||
|         ).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 绘制翻起页背面 | ||||
|      */ | ||||
|     private fun drawCurrentBackArea( | ||||
|         canvas: Canvas, | ||||
|         bitmap: Bitmap | ||||
|     ) { | ||||
|         val i = (mBezierStart1.x + mBezierControl1.x).toInt() / 2 | ||||
|         val f1 = abs(i - mBezierControl1.x) | ||||
|         val i1 = (mBezierStart2.y + mBezierControl2.y).toInt() / 2 | ||||
|         val f2 = abs(i1 - mBezierControl2.y) | ||||
|         val f3 = min(f1, f2) | ||||
|         mPath1.reset() | ||||
|         mPath1.moveTo(mBezierVertex2.x, mBezierVertex2.y) | ||||
|         mPath1.lineTo(mBezierVertex1.x, mBezierVertex1.y) | ||||
|         mPath1.lineTo(mBezierEnd1.x, mBezierEnd1.y) | ||||
|         mPath1.lineTo(touchX, touchY) | ||||
|         mPath1.lineTo(mBezierEnd2.x, mBezierEnd2.y) | ||||
|         mPath1.close() | ||||
|         val mFolderShadowDrawable: GradientDrawable | ||||
|         val left: Int | ||||
|         val right: Int | ||||
|         if (mIsRT_LB) { | ||||
|             left = (mBezierStart1.x - 1).toInt() | ||||
|             right = (mBezierStart1.x + f3 + 1).toInt() | ||||
|             mFolderShadowDrawable = mFolderShadowDrawableLR!! | ||||
|         } else { | ||||
|             left = (mBezierStart1.x - f3 - 1).toInt() | ||||
|             right = (mBezierStart1.x + 1).toInt() | ||||
|             mFolderShadowDrawable = mFolderShadowDrawableRL!! | ||||
|         } | ||||
|         canvas.save() | ||||
|         try { | ||||
|             canvas.clipPath(mPath0) | ||||
|             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|                 canvas.clipPath(mPath1) | ||||
|             } else { | ||||
|                 if (direction == Direction.NEXT) distanceX < 0 else distanceX > 0 | ||||
|                 canvas.clipPath(mPath1, Region.Op.INTERSECT) | ||||
|             } | ||||
|             isRunning = true | ||||
|             //设置触摸点 | ||||
|             setTouchPoint(e2.x, e2.y) | ||||
|         } catch (ignored: Exception) { | ||||
|         } | ||||
|         return isMoved | ||||
|         mPaint.colorFilter = mColorMatrixFilter | ||||
|         val dis = hypot( | ||||
|             mCornerX - mBezierControl1.x.toDouble(), | ||||
|             mBezierControl2.y - mCornerY.toDouble() | ||||
|         ).toFloat() | ||||
|         val f8 = (mCornerX - mBezierControl1.x) / dis | ||||
|         val f9 = (mBezierControl2.y - mCornerY) / dis | ||||
|         mMatrixArray[0] = 1 - 2 * f9 * f9 | ||||
|         mMatrixArray[1] = 2 * f8 * f9 | ||||
|         mMatrixArray[3] = mMatrixArray[1] | ||||
|         mMatrixArray[4] = 1 - 2 * f8 * f8 | ||||
|         mMatrix.reset() | ||||
|         mMatrix.setValues(mMatrixArray) | ||||
|         mMatrix.preTranslate(-mBezierControl1.x, -mBezierControl1.y) | ||||
|         mMatrix.postTranslate(mBezierControl1.x, mBezierControl1.y) | ||||
|         canvas.drawBitmap(bitmap, mMatrix, mPaint) | ||||
|         mPaint.colorFilter = null | ||||
|         canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y) | ||||
|         mFolderShadowDrawable.setBounds( | ||||
|             left, mBezierStart1.y.toInt(), right, | ||||
|             (mBezierStart1.y + mMaxLength).toInt() | ||||
|         ) | ||||
|         mFolderShadowDrawable.draw(canvas) | ||||
|         canvas.restore() | ||||
|     } | ||||
| 
 | ||||
|     override fun onPageUp() { | ||||
|         curlView?.updatePages() | ||||
|         curlView?.requestRender() | ||||
|     /** | ||||
|      * 绘制翻起页的阴影 | ||||
|      */ | ||||
|     private fun drawCurrentPageShadow(canvas: Canvas) { | ||||
|         val degree: Double = if (mIsRT_LB) { | ||||
|             (Math.PI / 4 - atan2(mBezierControl1.y - touchX, touchY - mBezierControl1.x)) | ||||
|         } else { | ||||
|             (Math.PI / 4 - atan2(touchY - mBezierControl1.y, touchX - mBezierControl1.x)) | ||||
|         } | ||||
|         // 翻起页阴影顶点与touch点的距离 | ||||
|         val d1 = 25.toFloat() * 1.414 * cos(degree) | ||||
|         val d2 = 25.toFloat() * 1.414 * sin(degree) | ||||
|         val x = (touchX + d1).toFloat() | ||||
|         val y: Float | ||||
|         y = if (mIsRT_LB) { | ||||
|             (touchY + d2).toFloat() | ||||
|         } else { | ||||
|             (touchY - d2).toFloat() | ||||
|         } | ||||
|         mPath1.reset() | ||||
|         mPath1.moveTo(x, y) | ||||
|         mPath1.lineTo(touchX, touchY) | ||||
|         mPath1.lineTo(mBezierControl1.x, mBezierControl1.y) | ||||
|         mPath1.lineTo(mBezierStart1.x, mBezierStart1.y) | ||||
|         mPath1.close() | ||||
|         canvas.save() | ||||
|         try { | ||||
|             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|                 canvas.clipOutPath(mPath0) | ||||
|             } else { | ||||
|                 canvas.clipPath(mPath0, Region.Op.XOR) | ||||
|             } | ||||
|             canvas.clipPath(mPath1, Region.Op.INTERSECT) | ||||
|         } catch (ignored: java.lang.Exception) { | ||||
|         } | ||||
|         var leftX: Int | ||||
|         var rightX: Int | ||||
|         var mCurrentPageShadow: GradientDrawable | ||||
|         if (mIsRT_LB) { | ||||
|             leftX = mBezierControl1.x.toInt() | ||||
|             rightX = mBezierControl1.x.toInt() + 25 | ||||
|             mCurrentPageShadow = mFrontShadowDrawableVLR!! | ||||
|         } else { | ||||
|             leftX = (mBezierControl1.x - 25).toInt() | ||||
|             rightX = mBezierControl1.x.toInt() + 1 | ||||
|             mCurrentPageShadow = mFrontShadowDrawableVRL!! | ||||
|         } | ||||
|         var rotateDegrees: Float = | ||||
|             Math.toDegrees(atan2(touchX - mBezierControl1.x, mBezierControl1.y - touchY).toDouble()) | ||||
|                 .toFloat() | ||||
|         canvas.rotate(rotateDegrees, mBezierControl1.x, mBezierControl1.y) | ||||
|         mCurrentPageShadow.setBounds( | ||||
|             leftX, | ||||
|             (mBezierControl1.y - mMaxLength).toInt(), rightX, | ||||
|             mBezierControl1.y.toInt() | ||||
|         ) | ||||
|         mCurrentPageShadow.draw(canvas) | ||||
|         canvas.restore() | ||||
|         mPath1.reset() | ||||
|         mPath1.moveTo(x, y) | ||||
|         mPath1.lineTo(touchX, touchY) | ||||
|         mPath1.lineTo(mBezierControl2.x, mBezierControl2.y) | ||||
|         mPath1.lineTo(mBezierStart2.x, mBezierStart2.y) | ||||
|         mPath1.close() | ||||
|         canvas.save() | ||||
|         try { | ||||
|             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|                 canvas.clipOutPath(mPath0) | ||||
|             } else { | ||||
|                 canvas.clipPath(mPath0, Region.Op.XOR) | ||||
|             } | ||||
|             canvas.clipPath(mPath1) | ||||
|         } catch (ignored: java.lang.Exception) { | ||||
|         } | ||||
|         if (mIsRT_LB) { | ||||
|             leftX = mBezierControl2.y.toInt() | ||||
|             rightX = (mBezierControl2.y + 25).toInt() | ||||
|             mCurrentPageShadow = mFrontShadowDrawableHTB!! | ||||
|         } else { | ||||
|             leftX = (mBezierControl2.y - 25).toInt() | ||||
|             rightX = (mBezierControl2.y + 1).toInt() | ||||
|             mCurrentPageShadow = mFrontShadowDrawableHBT!! | ||||
|         } | ||||
|         rotateDegrees = Math.toDegrees( | ||||
|             atan2(mBezierControl2.y - touchY, mBezierControl2.x - touchX).toDouble() | ||||
|         ).toFloat() | ||||
|         canvas.rotate(rotateDegrees, mBezierControl2.x, mBezierControl2.y) | ||||
|         val temp: Float = | ||||
|             if (mBezierControl2.y < 0) mBezierControl2.y - pageView.height else mBezierControl2.y | ||||
|         val hmg = hypot(mBezierControl2.x.toDouble(), temp.toDouble()).toInt() | ||||
|         if (hmg > mMaxLength) mCurrentPageShadow | ||||
|             .setBounds( | ||||
|                 (mBezierControl2.x - 25).toInt() - hmg, leftX, | ||||
|                 (mBezierControl2.x + mMaxLength).toInt() - hmg, | ||||
|                 rightX | ||||
|             ) else mCurrentPageShadow.setBounds( | ||||
|             (mBezierControl2.x - mMaxLength).toInt(), leftX, | ||||
|             mBezierControl2.x.toInt(), rightX | ||||
|         ) | ||||
|         mCurrentPageShadow.draw(canvas) | ||||
|         canvas.restore() | ||||
|     } | ||||
| 
 | ||||
|     override fun pageChange(change: Int) { | ||||
|         pageView.post { | ||||
|             if (change > 0) { | ||||
|                 pageView.moveToNextPage() | ||||
|     private fun drawNextPageAreaAndShadow( | ||||
|         canvas: Canvas, | ||||
|         bitmap: Bitmap | ||||
|     ) { | ||||
|         mPath1.reset() | ||||
|         mPath1.moveTo(mBezierStart1.x, mBezierStart1.y) | ||||
|         mPath1.lineTo(mBezierVertex1.x, mBezierVertex1.y) | ||||
|         mPath1.lineTo(mBezierVertex2.x, mBezierVertex2.y) | ||||
|         mPath1.lineTo(mBezierStart2.x, mBezierStart2.y) | ||||
|         mPath1.lineTo(mCornerX.toFloat(), mCornerY.toFloat()) | ||||
|         mPath1.close() | ||||
|         mDegrees = Math.toDegrees( | ||||
|             atan2( | ||||
|                 (mBezierControl1.x - mCornerX).toDouble(), | ||||
|                 mBezierControl2.y - mCornerY.toDouble() | ||||
|             ) | ||||
|         ).toFloat() | ||||
|         val leftX: Int | ||||
|         val rightY: Int | ||||
|         val mBackShadowDrawable: GradientDrawable | ||||
|         if (mIsRT_LB) { //左下及右上 | ||||
|             leftX = mBezierStart1.x.toInt() | ||||
|             rightY = (mBezierStart1.x + mTouchToCornerDis / 4).toInt() | ||||
|             mBackShadowDrawable = mBackShadowDrawableLR!! | ||||
|         } else { | ||||
|             leftX = (mBezierStart1.x - mTouchToCornerDis / 4).toInt() | ||||
|             rightY = mBezierStart1.x.toInt() | ||||
|             mBackShadowDrawable = mBackShadowDrawableRL!! | ||||
|         } | ||||
|         canvas.save() | ||||
|         try { | ||||
|             canvas.clipPath(mPath0) | ||||
|             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|                 canvas.clipPath(mPath1) | ||||
|             } else { | ||||
|                 pageView.moveToPrevPage() | ||||
|                 canvas.clipPath(mPath1, Region.Op.INTERSECT) | ||||
|             } | ||||
|             //canvas.clipPath(mPath1, Region.Op.INTERSECT); | ||||
|         } catch (ignored: java.lang.Exception) { | ||||
|         } | ||||
|         canvas.drawBitmap(bitmap, 0f, 0f, null) | ||||
|         canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y) | ||||
|         mBackShadowDrawable.setBounds( | ||||
|             leftX, mBezierStart1.y.toInt(), rightY, | ||||
|             (mMaxLength + mBezierStart1.y).toInt() | ||||
|         ) //左上及右下角的xy坐标值,构成一个矩形 | ||||
|         mBackShadowDrawable.draw(canvas) | ||||
|         canvas.restore() | ||||
|     } | ||||
| 
 | ||||
|     private inner class PageProvider : CurlView.PageProvider { | ||||
|     private fun drawCurrentPageArea( | ||||
|         canvas: Canvas, | ||||
|         bitmap: Bitmap, | ||||
|         path: Path | ||||
|     ) { | ||||
|         mPath0.reset() | ||||
|         mPath0.moveTo(mBezierStart1.x, mBezierStart1.y) | ||||
|         mPath0.quadTo( | ||||
|             mBezierControl1.x, mBezierControl1.y, mBezierEnd1.x, | ||||
|             mBezierEnd1.y | ||||
|         ) | ||||
|         mPath0.lineTo(touchX, touchY) | ||||
|         mPath0.lineTo(mBezierEnd2.x, mBezierEnd2.y) | ||||
|         mPath0.quadTo( | ||||
|             mBezierControl2.x, mBezierControl2.y, mBezierStart2.x, | ||||
|             mBezierStart2.y | ||||
|         ) | ||||
|         mPath0.lineTo(mCornerX.toFloat(), mCornerY.toFloat()) | ||||
|         mPath0.close() | ||||
|         canvas.save() | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|             canvas.clipOutPath(path) | ||||
|         } else { | ||||
|             canvas.clipPath(path, Region.Op.XOR) | ||||
|         } | ||||
|         canvas.drawBitmap(bitmap, 0f, 0f, null) | ||||
|         try { | ||||
|             canvas.restore() | ||||
|         } catch (e: java.lang.Exception) { | ||||
|             e.printStackTrace() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|         override val pageCount: Int | ||||
|             get() = 3 | ||||
|     /** | ||||
|      * 计算拖拽点对应的拖拽脚 | ||||
|      */ | ||||
|     private fun calcCornerXY(x: Float, y: Float) { | ||||
|         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) { | ||||
|             when (index) { | ||||
|                 0 -> page.setTexture(prevPage?.screenshot(), CurlPage.SIDE_BOTH) | ||||
|                 1 -> page.setTexture(curPage?.screenshot(), CurlPage.SIDE_BOTH) | ||||
|                 2 -> page.setTexture(nextPage?.screenshot(), CurlPage.SIDE_BOTH) | ||||
|     private fun calcPoints() { | ||||
|         mMiddleX = (touchX + mCornerX) / 2 | ||||
|         mMiddleY = (touchY + mCornerY) / 2 | ||||
|         mBezierControl1.x = | ||||
|             mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX) | ||||
|         mBezierControl1.y = mCornerY.toFloat() | ||||
|         mBezierControl2.x = mCornerX.toFloat() | ||||
|         val f4 = mCornerY - mMiddleY | ||||
|         if (f4 == 0f) { | ||||
|             mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f | ||||
|         } else { | ||||
|             mBezierControl2.y = | ||||
|                 mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY) | ||||
|         } | ||||
|         mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2 | ||||
|         mBezierStart1.y = mCornerY.toFloat() | ||||
|         // 当mBezierStart1.x < 0或者mBezierStart1.x > 480时 | ||||
|         // 如果继续翻页,会出现BUG故在此限制 | ||||
|         if (touchX > 0 && touchX < pageView.width) { | ||||
|             if (mBezierStart1.x < 0 || mBezierStart1.x > pageView.width) { | ||||
|                 if (mBezierStart1.x < 0) mBezierStart1.x = pageView.width - mBezierStart1.x | ||||
|                 val f1: Float = abs(mCornerX - touchX) | ||||
|                 val f2: Float = pageView.width * f1 / mBezierStart1.x | ||||
|                 touchX = abs(mCornerX - f2) | ||||
|                 val f3: Float = abs(mCornerX - touchX) * abs(mCornerY - touchX) / f1 | ||||
|                 touchX = abs(mCornerY - f3) | ||||
|                 mMiddleX = (touchX + mCornerX) / 2 | ||||
|                 mMiddleY = (touchY + mCornerY) / 2 | ||||
|                 mBezierControl1.x = | ||||
|                     mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX) | ||||
|                 mBezierControl1.y = mCornerY.toFloat() | ||||
|                 mBezierControl2.x = mCornerX.toFloat() | ||||
|                 val f5 = mCornerY - mMiddleY | ||||
|                 if (f5 == 0f) { | ||||
|                     mBezierControl2.y = | ||||
|                         mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f | ||||
|                 } else { | ||||
|                     mBezierControl2.y = | ||||
|                         mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY) | ||||
|                 } | ||||
|                 mBezierStart1.x = (mBezierControl1.x | ||||
|                         - (mCornerX - mBezierControl1.x) / 2) | ||||
|             } | ||||
|         } | ||||
|         mBezierStart2.x = mCornerX.toFloat() | ||||
|         mBezierStart2.y = mBezierControl2.y - (mCornerY - mBezierControl2.y) / 2 | ||||
|         mTouchToCornerDis = hypot(touchX - mCornerX, touchY - mCornerY) | ||||
|         mBezierEnd1 = getCross( | ||||
|             PointF(touchX, touchY), mBezierControl1, mBezierStart1, | ||||
|             mBezierStart2 | ||||
|         ) | ||||
|         mBezierEnd2 = getCross( | ||||
|             PointF(touchX, touchY), mBezierControl2, mBezierStart1, | ||||
|             mBezierStart2 | ||||
|         ) | ||||
|         mBezierVertex1.x = (mBezierStart1.x + 2 * mBezierControl1.x + mBezierEnd1.x) / 4 | ||||
|         mBezierVertex1.y = (2 * mBezierControl1.y + mBezierStart1.y + mBezierEnd1.y) / 4 | ||||
|         mBezierVertex2.x = (mBezierStart2.x + 2 * mBezierControl2.x + mBezierEnd2.x) / 4 | ||||
|         mBezierVertex2.y = (2 * mBezierControl2.y + mBezierStart2.y + mBezierEnd2.y) / 4 | ||||
|     } | ||||
| 
 | ||||
|     // 定义书籍尺寸的变化监听器 | ||||
|     private inner class SizeChangedObserver : CurlView.SizeChangedObserver { | ||||
|         override fun onSizeChanged(width: Int, height: Int) { | ||||
|             curlView?.setViewMode(CurlView.SHOW_ONE_PAGE) | ||||
|         } | ||||
|     /** | ||||
|      * 求解直线P1P2和直线P3P4的交点坐标 | ||||
|      */ | ||||
|     private fun getCross(P1: PointF, P2: PointF, P3: PointF, P4: PointF): PointF { | ||||
|         val crossP = PointF() | ||||
|         // 二元函数通式: y=ax+b | ||||
|         val a1 = (P2.y - P1.y) / (P2.x - P1.x) | ||||
|         val b1 = (P1.x * P2.y - P2.x * P1.y) / (P1.x - P2.x) | ||||
|         val a2 = (P4.y - P3.y) / (P4.x - P3.x) | ||||
|         val b2 = (P3.x * P4.y - P4.x * P3.y) / (P3.x - P4.x) | ||||
|         crossP.x = (b2 - b1) / (a1 - a2) | ||||
|         crossP.y = a1 * crossP.x + b1 | ||||
|         return crossP | ||||
|     } | ||||
| } | ||||
| @ -1,18 +1,36 @@ | ||||
| package io.legado.app.ui.download | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.view.View | ||||
| import io.legado.app.R | ||||
| import io.legado.app.base.adapter.ItemViewHolder | ||||
| import io.legado.app.base.adapter.SimpleRecyclerAdapter | ||||
| import io.legado.app.data.entities.Book | ||||
| import io.legado.app.help.BookHelp | ||||
| import kotlinx.android.synthetic.main.item_download.view.* | ||||
| 
 | ||||
| 
 | ||||
| class DownloadAdapter(context: Context) : | ||||
|     SimpleRecyclerAdapter<Book>(context, R.layout.item_download) { | ||||
| 
 | ||||
| 
 | ||||
|     override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList<Any>) { | ||||
|         with(holder.itemView) { | ||||
|             if (payloads.isEmpty()) { | ||||
|                 tv_name.text = item.name | ||||
|                 tv_author.text = item.author | ||||
|                 upDownloadCount(this, item) | ||||
|             } else { | ||||
|                 upDownloadCount(this, item) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun upDownloadCount(view: View, book: Book) { | ||||
|         view.tv_download.text = context.getString( | ||||
|             R.string.download_count, | ||||
|             BookHelp.getChapterCount(book), | ||||
|             book.totalChapterNum | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -1,86 +1,221 @@ | ||||
| package io.legado.app.ui.widget.font | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.Context | ||||
| import android.app.Activity.RESULT_OK | ||||
| import android.content.Intent | ||||
| import android.net.Uri | ||||
| import android.os.Bundle | ||||
| import android.os.Environment | ||||
| import android.util.DisplayMetrics | ||||
| import android.view.LayoutInflater | ||||
| import android.view.MenuItem | ||||
| import android.view.View | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import android.view.ViewGroup | ||||
| import androidx.appcompat.widget.Toolbar | ||||
| import androidx.documentfile.provider.DocumentFile | ||||
| import androidx.fragment.app.DialogFragment | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import io.legado.app.App | ||||
| import io.legado.app.R | ||||
| import io.legado.app.lib.dialogs.AlertBuilder | ||||
| import io.legado.app.lib.dialogs.alert | ||||
| import io.legado.app.utils.applyTint | ||||
| import io.legado.app.utils.invisible | ||||
| import io.legado.app.utils.visible | ||||
| import kotlinx.android.synthetic.main.dialog_font_select.view.* | ||||
| import io.legado.app.constant.PreferKey | ||||
| import io.legado.app.help.FileHelp | ||||
| import io.legado.app.help.permission.Permissions | ||||
| import io.legado.app.help.permission.PermissionsCompat | ||||
| import io.legado.app.utils.DocumentUtils | ||||
| import io.legado.app.utils.getPrefString | ||||
| import io.legado.app.utils.putPrefString | ||||
| import io.legado.app.utils.toast | ||||
| import kotlinx.android.synthetic.main.dialog_font_select.* | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers.IO | ||||
| import kotlinx.coroutines.Dispatchers.Main | ||||
| import kotlinx.coroutines.Job | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.withContext | ||||
| import java.io.File | ||||
| import kotlin.coroutines.CoroutineContext | ||||
| 
 | ||||
| class FontSelectDialog(context: Context) : FontAdapter.CallBack { | ||||
| 
 | ||||
|     private val defaultFolder = | ||||
|         Environment.getExternalStorageDirectory().absolutePath + File.separator + "Fonts" | ||||
| class FontSelectDialog : DialogFragment(), | ||||
|     Toolbar.OnMenuItemClickListener, | ||||
|     CoroutineScope, | ||||
|     FontAdapter.CallBack { | ||||
|     lateinit var job: Job | ||||
|     private val fontFolderRequestCode = 35485 | ||||
|     private lateinit var adapter: FontAdapter | ||||
|     private var builder: AlertBuilder<AlertDialog> | ||||
|     private var dialog: AlertDialog? = null | ||||
|     @SuppressLint("InflateParams") | ||||
|     private var view: View = LayoutInflater.from(context).inflate(R.layout.dialog_font_select, null) | ||||
|     var curPath: String? = null | ||||
|     var fontFolder: String? = null | ||||
|     var defaultFont: (() -> Unit)? = null | ||||
|     var selectFile: ((path: String) -> Unit)? = null | ||||
|     private val fontFolder = | ||||
|         App.INSTANCE.filesDir.absolutePath + File.separator + "Fonts" + File.separator | ||||
|     override val coroutineContext: CoroutineContext | ||||
|         get() = job + Main | ||||
| 
 | ||||
|     init { | ||||
|         builder = context.alert(title = context.getString(R.string.select_font)) { | ||||
|             customView = view | ||||
|             positiveButton(R.string.default_font) { defaultFont?.invoke() } | ||||
|             negativeButton(R.string.cancel) | ||||
|         } | ||||
|         initData() | ||||
|     override fun onStart() { | ||||
|         super.onStart() | ||||
|         val dm = DisplayMetrics() | ||||
|         activity?.windowManager?.defaultDisplay?.getMetrics(dm) | ||||
|         dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) | ||||
|     } | ||||
| 
 | ||||
|     fun show() { | ||||
|         dialog = builder.show().applyTint() | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, | ||||
|         container: ViewGroup?, | ||||
|         savedInstanceState: Bundle? | ||||
|     ): View? { | ||||
|         job = Job() | ||||
|         return inflater.inflate(R.layout.dialog_font_select, container) | ||||
|     } | ||||
| 
 | ||||
|     private fun initData() = with(view) { | ||||
|         adapter = FontAdapter(context, this@FontSelectDialog) | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|         tool_bar.setTitle(R.string.select_font) | ||||
|         tool_bar.inflateMenu(R.menu.font_select) | ||||
|         tool_bar.setOnMenuItemClickListener(this) | ||||
|         adapter = FontAdapter(requireContext(), this) | ||||
|         recycler_view.layoutManager = LinearLayoutManager(context) | ||||
|         recycler_view.adapter = adapter | ||||
|         val files = getFontFiles() | ||||
|         if (files.isNullOrEmpty()) { | ||||
|             tv_no_data.visible() | ||||
| 
 | ||||
|         val fontPath = getPrefString(PreferKey.fontFolder) | ||||
|         if (fontPath.isNullOrEmpty()) { | ||||
|             openFolder() | ||||
|         } else { | ||||
|             tv_no_data.invisible() | ||||
|             adapter.setItems(files.toList()) | ||||
|             val uri = Uri.parse(fontPath) | ||||
|             if (DocumentFile.fromTreeUri(requireContext(), uri)?.canRead() == true) { | ||||
|                 getFontFiles(uri) | ||||
|             } else { | ||||
|                 openFolder() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onMenuItemClick(item: MenuItem?): Boolean { | ||||
|         when (item?.itemId) { | ||||
|             R.id.menu_default -> { | ||||
|                 val pf = parentFragment | ||||
|                 if (pf is CallBack) { | ||||
|                     if ("" != pf.curFontPath) { | ||||
|                         pf.selectFile("") | ||||
|                     } | ||||
|                 } | ||||
|                 val activity = activity | ||||
|                 if (activity is CallBack) { | ||||
|                     if ("" != activity.curFontPath) { | ||||
|                         activity.selectFile("") | ||||
|                     } | ||||
|                 } | ||||
|                 dismiss() | ||||
|             } | ||||
|             R.id.menu_other -> { | ||||
|                 openFolder() | ||||
|             } | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroy() { | ||||
|         super.onDestroy() | ||||
|         job.cancel() | ||||
|     } | ||||
| 
 | ||||
|     private fun openFolder() { | ||||
|         try { | ||||
|             val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) | ||||
|             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) | ||||
|             startActivityForResult(intent, fontFolderRequestCode) | ||||
|         } catch (e: java.lang.Exception) { | ||||
|             PermissionsCompat.Builder(this) | ||||
|                 .addPermissions(*Permissions.Group.STORAGE) | ||||
|                 .rationale(R.string.tip_perm_request_storage) | ||||
|                 .onGranted { getFontFilesOld() } | ||||
|                 .request() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @SuppressLint("DefaultLocale") | ||||
|     private fun getFontFiles(): Array<File>? { | ||||
|         val path = if (fontFolder.isNullOrEmpty()) { | ||||
|             defaultFolder | ||||
|         } else fontFolder | ||||
|         return try { | ||||
|             val file = File(path) | ||||
|     private fun getFontFiles(uri: Uri) { | ||||
|         launch(IO) { | ||||
|             DocumentFile.fromTreeUri(requireContext(), uri)?.listFiles()?.forEach { file -> | ||||
|                 if (file.name?.toLowerCase()?.matches(".*\\.[ot]tf".toRegex()) == true) { | ||||
|                     DocumentUtils.readBytes(App.INSTANCE, file.uri)?.let { | ||||
|                         FileHelp.getFile(fontFolder + file.name).writeBytes(it) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             try { | ||||
|                 val file = File(fontFolder) | ||||
|                 file.listFiles { pathName -> | ||||
|                     pathName.name.toLowerCase().matches(".*\\.[ot]tf".toRegex()) | ||||
|                 }?.let { | ||||
|                     withContext(Main) { | ||||
|                         adapter.setItems(it.toList()) | ||||
|                     } | ||||
|                 } | ||||
|             } catch (e: Exception) { | ||||
|                 toast(e.localizedMessage ?: "") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @SuppressLint("DefaultLocale") | ||||
|     private fun getFontFilesOld() { | ||||
|         try { | ||||
|             val file = | ||||
|                 File(Environment.getExternalStorageDirectory().absolutePath + File.separator + "Fonts") | ||||
|             file.listFiles { pathName -> | ||||
|                 pathName.name.toLowerCase().matches(".*\\.[ot]tf".toRegex()) | ||||
|             }?.let { | ||||
|                 adapter.setItems(it.toList()) | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|             null | ||||
|             toast(e.localizedMessage ?: "") | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onClick(file: File) { | ||||
|         file.absolutePath.let { | ||||
|             if (it != curPath) { | ||||
|                 selectFile?.invoke(it) | ||||
|                 dialog?.dismiss() | ||||
|             val pf = parentFragment | ||||
|             if (pf is CallBack) { | ||||
|                 if (it != pf.curFontPath) { | ||||
|                     pf.selectFile(it) | ||||
|                 } | ||||
|             } | ||||
|             val activity = activity | ||||
|             if (activity is CallBack) { | ||||
|                 if (it != activity.curFontPath) { | ||||
|                     activity.selectFile(it) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         dialog?.dismiss() | ||||
|     } | ||||
| 
 | ||||
|     override fun curFilePath(): String { | ||||
|         return curPath ?: "" | ||||
|         val pf = parentFragment | ||||
|         if (pf is CallBack) { | ||||
|             return pf.curFontPath | ||||
|         } | ||||
|         val activity = activity | ||||
|         if (activity is CallBack) { | ||||
|             return activity.curFontPath | ||||
|         } | ||||
|         return "" | ||||
|     } | ||||
| 
 | ||||
|     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { | ||||
|         super.onActivityResult(requestCode, resultCode, data) | ||||
|         when (requestCode) { | ||||
|             fontFolderRequestCode -> if (resultCode == RESULT_OK) { | ||||
|                 data?.data?.let { uri -> | ||||
|                     putPrefString(PreferKey.fontFolder, uri.toString()) | ||||
|                     context?.contentResolver?.takePersistableUriPermission( | ||||
|                         uri, | ||||
|                         Intent.FLAG_GRANT_READ_URI_PERMISSION | ||||
|                     ) | ||||
|                     getFontFiles(uri) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     interface CallBack { | ||||
|         fun selectFile(path: String) | ||||
|         val curFontPath: String | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,52 @@ | ||||
| package io.legado.app.utils | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.net.Uri | ||||
| 
 | ||||
| object DocumentUtils { | ||||
| 
 | ||||
|     @JvmStatic | ||||
|     fun writeText(context: Context, data: String, fileUri: Uri): Boolean { | ||||
|         return writeBytes(context, data.toByteArray(), fileUri) | ||||
|     } | ||||
| 
 | ||||
|     @JvmStatic | ||||
|     fun writeBytes(context: Context, data: ByteArray, fileUri: Uri): Boolean { | ||||
|         try { | ||||
|             context.contentResolver.openOutputStream(fileUri)?.let { | ||||
|                 it.write(data) | ||||
|                 it.close() | ||||
|                 return true | ||||
|             } | ||||
|         } catch (e: java.lang.Exception) { | ||||
|             e.printStackTrace() | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
| 
 | ||||
|     @JvmStatic | ||||
|     fun readText(context: Context, uri: Uri): String? { | ||||
|         readBytes(context, uri)?.let { | ||||
|             return String(it) | ||||
|         } | ||||
|         return null | ||||
|     } | ||||
| 
 | ||||
|     @JvmStatic | ||||
|     fun readBytes(context: Context, uri: Uri): ByteArray? { | ||||
|         try { | ||||
|             context.contentResolver.openInputStream(uri)?.let { | ||||
|                 val len: Int = it.available() | ||||
|                 val buffer = ByteArray(len) | ||||
|                 it.read(buffer) | ||||
|                 it.close() | ||||
|                 return buffer | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|             e.printStackTrace() | ||||
|         } | ||||
|         return null | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
| Before Width: | Height: | Size: 3.1 KiB | 
| @ -0,0 +1,8 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <shape xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <!--实现应用背景颜色渐变--> | ||||
|     <gradient | ||||
|         android:startColor="#17000000" | ||||
|         android:endColor="#00000000" | ||||
|         android:angle="90" /> | ||||
| </shape> | ||||
| Before Width: | Height: | Size: 418 B | 
| @ -0,0 +1,8 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <shape xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <!--实现应用背景颜色渐变--> | ||||
|     <gradient | ||||
|         android:startColor="#17FFFFFF" | ||||
|         android:endColor="#00FFFFFF" | ||||
|         android:angle="90" /> | ||||
| </shape> | ||||
| Before Width: | Height: | Size: 340 B | 
| @ -0,0 +1,8 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <shape xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <!--实现应用背景颜色渐变--> | ||||
|     <gradient | ||||
|         android:startColor="#17000000" | ||||
|         android:endColor="#00000000" | ||||
|         android:angle="270" /> | ||||
| </shape> | ||||
| Before Width: | Height: | Size: 424 B | 
| @ -0,0 +1,8 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <shape xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <!--实现应用背景颜色渐变--> | ||||
|     <gradient | ||||
|         android:startColor="#17FFFFFF" | ||||
|         android:endColor="#00FFFFFF" | ||||
|         android:angle="270" /> | ||||
| </shape> | ||||
| @ -1,9 +0,0 @@ | ||||
| <shape xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:shape="rectangle"> | ||||
|     <gradient | ||||
|         android:angle="135" | ||||
|         android:centerColor="#009688" | ||||
|         android:endColor="#00695C" | ||||
|         android:startColor="#4DB6AC" | ||||
|         android:type="linear" /> | ||||
| </shape> | ||||
| @ -1,20 +1,18 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:orientation="vertical" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:padding="16dp"> | ||||
| 
 | ||||
|     <androidx.recyclerview.widget.RecyclerView | ||||
|         android:id="@+id/recycler_view" | ||||
|     <androidx.appcompat.widget.Toolbar | ||||
|         android:id="@+id/tool_bar" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" /> | ||||
| 
 | ||||
|     <TextView | ||||
|         android:id="@+id/tv_no_data" | ||||
|     <androidx.recyclerview.widget.RecyclerView | ||||
|         android:id="@+id/recycler_view" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:padding="16dp" | ||||
|         android:text="@string/fonts_folder" /> | ||||
|         android:layout_height="match_parent" /> | ||||
| 
 | ||||
| </FrameLayout> | ||||
| </LinearLayout> | ||||
| @ -0,0 +1,11 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <menu xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||
| 
 | ||||
|     <item | ||||
|         android:id="@+id/menu_download" | ||||
|         android:title="@string/action_download" | ||||
|         android:icon="@drawable/ic_play_24dp" | ||||
|         app:showAsAction="ifRoom" /> | ||||
| 
 | ||||
| </menu> | ||||
| @ -0,0 +1,15 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <menu xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||
| 
 | ||||
|     <item | ||||
|         android:id="@+id/menu_default" | ||||
|         android:title="@string/default_font" | ||||
|         app:showAsAction="ifRoom" /> | ||||
| 
 | ||||
|     <item | ||||
|         android:id="@+id/menu_other" | ||||
|         android:title="@string/other_folder" | ||||
|         app:showAsAction="never" /> | ||||
| 
 | ||||
| </menu> | ||||
					Loading…
					
					
				
		Reference in new issue