diff --git a/app/src/main/java/io/legado/app/ui/widget/page/PageView.kt b/app/src/main/java/io/legado/app/ui/widget/page/PageView.kt index 1c293e74e..894562639 100644 --- a/app/src/main/java/io/legado/app/ui/widget/page/PageView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/page/PageView.kt @@ -7,6 +7,7 @@ import android.util.AttributeSet import android.view.MotionEvent import android.widget.FrameLayout import io.legado.app.help.ReadBookConfig +import io.legado.app.ui.widget.page.curl.CurlView import io.legado.app.ui.widget.page.delegate.* import io.legado.app.utils.activity import io.legado.app.utils.getPrefInt @@ -23,6 +24,7 @@ class PageView(context: Context, attrs: AttributeSet) : var prevPage: ContentView? = null var curPage: ContentView? = null var nextPage: ContentView? = null + var curlView: CurlView? = null init { callback = activity as? CallBack @@ -85,6 +87,10 @@ class PageView(context: Context, attrs: AttributeSet) : } fun upPageAnim() { + if (curlView != null) { + removeView(curlView) + curlView = null + } pageDelegate = null pageDelegate = when (context.getPrefInt("pageAnim")) { 0 -> CoverPageDelegate(this) diff --git a/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlMesh.java b/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlMesh.java index 448de5bce..6efcf75bf 100644 --- a/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlMesh.java +++ b/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlMesh.java @@ -95,21 +95,21 @@ public class CurlMesh { // 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); + 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( + mArrSelfShadowVertices = new Array<>( (mMaxCurlSplits + 2) * 2); - mArrDropShadowVertices = new Array( + mArrDropShadowVertices = new Array<>( (mMaxCurlSplits + 2) * 2); - mArrTempShadowVertices = new Array( + mArrTempShadowVertices = new Array<>( (mMaxCurlSplits + 2) * 2); for (int i = 0; i < (mMaxCurlSplits + 2) * 2; ++i) { mArrTempShadowVertices.add(new ShadowVertex()); diff --git a/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlPage.java b/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlPage.java deleted file mode 100644 index e2ea847db..000000000 --- a/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlPage.java +++ /dev/null @@ -1,195 +0,0 @@ -package io.legado.app.ui.widget.page.curl; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.RectF; - -/** - * Storage class for page textures, blend colors and possibly some other values - * in the future. - * - * @author harism - */ -public class CurlPage { - - public static final int SIDE_BACK = 2; - public static final int SIDE_BOTH = 3; - public static final int SIDE_FRONT = 1; - - private int mColorBack; - private int mColorFront; - private Bitmap mTextureBack; - private Bitmap mTextureFront; - private boolean mTexturesChanged; - - /** - * Default constructor. - */ - public CurlPage() { - reset(); - } - - /** - * Getter for color. - */ - public int getColor(int side) { - switch (side) { - case SIDE_FRONT: - return mColorFront; - default: - return mColorBack; - } - } - - /** - * Calculates the next highest power of two for a given integer. - */ - private int getNextHighestPO2(int n) { - n -= 1; - n = n | (n >> 1); - n = n | (n >> 2); - n = n | (n >> 4); - n = n | (n >> 8); - n = n | (n >> 16); - n = n | (n >> 32); - return n + 1; - } - - /** - * Generates nearest power of two sized Bitmap for give Bitmap. Returns this - * new Bitmap using default return statement + original texture coordinates - * are stored into RectF. - */ - private Bitmap getTexture(Bitmap bitmap, RectF textureRect) { - // Bitmap original size. - int w = bitmap.getWidth(); - int h = bitmap.getHeight(); - // Bitmap size expanded to next power of two. This is done due to - // the requirement on many devices, texture width and height should - // be power of two. - int newW = getNextHighestPO2(w); - int newH = getNextHighestPO2(h); - - // TODO: Is there another way to create a bigger Bitmap and copy - // original Bitmap to it more efficiently? Immutable bitmap anyone? - Bitmap bitmapTex = Bitmap.createBitmap(newW, newH, bitmap.getConfig()); - Canvas c = new Canvas(bitmapTex); - c.drawBitmap(bitmap, 0, 0, null); - - // Calculate final texture coordinates. - float texX = (float) w / newW; - float texY = (float) h / newH; - textureRect.set(0f, 0f, texX, texY); - - return bitmapTex; - } - - /** - * Getter for textures. Creates Bitmap sized to nearest power of two, copies - * original Bitmap into it and returns it. RectF given as parameter is - * filled with actual texture coordinates in this new upscaled texture - * Bitmap. - */ - public Bitmap getTexture(RectF textureRect, int side) { - switch (side) { - case SIDE_FRONT: - return getTexture(mTextureFront, textureRect); - default: - return getTexture(mTextureBack, textureRect); - } - } - - /** - * Returns true if textures have changed. - */ - public boolean getTexturesChanged() { - return mTexturesChanged; - } - - /** - * Returns true if back siding texture exists and it differs from front - * facing one. - */ - public boolean hasBackTexture() { - return !mTextureFront.equals(mTextureBack); - } - - /** - * Recycles and frees underlying Bitmaps. - */ - public void recycle() { - if (mTextureFront != null) { - mTextureFront.recycle(); - } - mTextureFront = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565); - mTextureFront.eraseColor(mColorFront); - if (mTextureBack != null) { - mTextureBack.recycle(); - } - mTextureBack = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565); - mTextureBack.eraseColor(mColorBack); - mTexturesChanged = false; - } - - /** - * Resets this CurlPage into its initial state. - */ - public void reset() { - mColorBack = Color.WHITE; - mColorFront = Color.WHITE; - recycle(); - } - - /** - * Setter blend color. - */ - public void setColor(int color, int side) { - switch (side) { - case SIDE_FRONT: - mColorFront = color; - break; - case SIDE_BACK: - mColorBack = color; - break; - default: - mColorFront = mColorBack = color; - break; - } - } - - /** - * Setter for textures. - */ - public void setTexture(Bitmap texture, int side) { - if (texture == null) { - texture = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565); - if (side == SIDE_BACK) { - texture.eraseColor(mColorBack); - } else { - texture.eraseColor(mColorFront); - } - } - switch (side) { - case SIDE_FRONT: - if (mTextureFront != null) - mTextureFront.recycle(); - mTextureFront = texture; - break; - case SIDE_BACK: - if (mTextureBack != null) - mTextureBack.recycle(); - mTextureBack = texture; - break; - case SIDE_BOTH: - if (mTextureFront != null) - mTextureFront.recycle(); - if (mTextureBack != null) - mTextureBack.recycle(); - mTextureFront = mTextureBack = texture; - break; - } - mTexturesChanged = true; - } - -} diff --git a/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlPage.kt b/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlPage.kt new file mode 100644 index 000000000..5f6ad5bb1 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlPage.kt @@ -0,0 +1,191 @@ +package io.legado.app.ui.widget.page.curl + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.RectF + +/** + * Storage class for page textures, blend colors and possibly some other values + * in the future. + * + * @author harism + */ +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 + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlRenderer.java b/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlRenderer.java deleted file mode 100644 index 7b18c744d..000000000 --- a/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlRenderer.java +++ /dev/null @@ -1,250 +0,0 @@ -package io.legado.app.ui.widget.page.curl; - -import android.graphics.Color; -import android.graphics.PointF; -import android.graphics.RectF; -import android.opengl.GLSurfaceView; -import android.opengl.GLU; - -import java.util.Vector; - -import javax.microedition.khronos.egl.EGLConfig; -import javax.microedition.khronos.opengles.GL10; - -/** - * Actual renderer class. - * - * @author harism - */ -public class CurlRenderer implements GLSurfaceView.Renderer { - - // Constant for requesting left page rect. - public static final int PAGE_LEFT = 1; - // Constant for requesting right page rect. - public static final int PAGE_RIGHT = 2; - // Constants for changing view mode. - public static final int SHOW_ONE_PAGE = 1; - public static final int SHOW_TWO_PAGES = 2; - // Set to true for checking quickly how perspective projection looks. - private static final boolean USE_PERSPECTIVE_PROJECTION = false; - // Background fill color. - private int mBackgroundColor; - // Curl meshes used for static and dynamic rendering. - private Vector mCurlMeshes; - private RectF mMargins = new RectF(); - private CurlRenderer.Observer mObserver; - // Page rectangles. - private RectF mPageRectLeft; - private RectF mPageRectRight; - // View mode. - private int mViewMode = SHOW_ONE_PAGE; - // Screen size. - private int mViewportWidth, mViewportHeight; - // Rect for render area. - private RectF mViewRect = new RectF(); - - /** - * Basic constructor. - */ - public CurlRenderer(CurlRenderer.Observer observer) { - mObserver = observer; - mCurlMeshes = new Vector(); - mPageRectLeft = new RectF(); - mPageRectRight = new RectF(); - } - - /** - * Adds CurlMesh to this renderer. - */ - public synchronized void addCurlMesh(CurlMesh mesh) { - removeCurlMesh(mesh); - mCurlMeshes.add(mesh); - } - - /** - * Returns rect reserved for left or right page. Value page should be - * PAGE_LEFT or PAGE_RIGHT. - */ - public RectF getPageRect(int page) { - if (page == PAGE_LEFT) { - return mPageRectLeft; - } else if (page == PAGE_RIGHT) { - return mPageRectRight; - } - return null; - } - - @Override - public synchronized void onDrawFrame(GL10 gl) { - - mObserver.onDrawFrame(); - - gl.glClearColor(Color.red(mBackgroundColor) / 255f, - Color.green(mBackgroundColor) / 255f, - Color.blue(mBackgroundColor) / 255f, - Color.alpha(mBackgroundColor) / 255f); - gl.glClear(GL10.GL_COLOR_BUFFER_BIT); - gl.glLoadIdentity(); - - if (USE_PERSPECTIVE_PROJECTION) { - gl.glTranslatef(0, 0, -6f); - } - - for (int i = 0; i < mCurlMeshes.size(); ++i) { - mCurlMeshes.get(i).onDrawFrame(gl); - } - } - - @Override - public void onSurfaceChanged(GL10 gl, int width, int height) { - gl.glViewport(0, 0, width, height); - mViewportWidth = width; - mViewportHeight = height; - - float ratio = (float) width / height; - mViewRect.top = 1.0f; - mViewRect.bottom = -1.0f; - mViewRect.left = -ratio; - mViewRect.right = ratio; - updatePageRects(); - - gl.glMatrixMode(GL10.GL_PROJECTION); - gl.glLoadIdentity(); - if (USE_PERSPECTIVE_PROJECTION) { - GLU.gluPerspective(gl, 20f, (float) width / height, .1f, 100f); - } else { - GLU.gluOrtho2D(gl, mViewRect.left, mViewRect.right, - mViewRect.bottom, mViewRect.top); - } - - gl.glMatrixMode(GL10.GL_MODELVIEW); - gl.glLoadIdentity(); - } - - @Override - public void onSurfaceCreated(GL10 gl, EGLConfig config) { - gl.glClearColor(0f, 0f, 0f, 1f); - gl.glShadeModel(GL10.GL_SMOOTH); - gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST); - gl.glHint(GL10.GL_LINE_SMOOTH_HINT, GL10.GL_NICEST); - gl.glHint(GL10.GL_POLYGON_SMOOTH_HINT, GL10.GL_NICEST); - gl.glEnable(GL10.GL_LINE_SMOOTH); - gl.glDisable(GL10.GL_DEPTH_TEST); - gl.glDisable(GL10.GL_CULL_FACE); - - mObserver.onSurfaceCreated(); - } - - /** - * Removes CurlMesh from this renderer. - */ - public synchronized void removeCurlMesh(CurlMesh mesh) { - while (mCurlMeshes.remove(mesh)) - ; - } - - /** - * Change background/clear color. - */ - public void setBackgroundColor(int color) { - mBackgroundColor = color; - } - - /** - * Set margins or padding. Note: margins are proportional. Meaning a value - * of .1f will produce a 10% margin. - */ - public synchronized void setMargins(float left, float top, float right, - float bottom) { - mMargins.left = left; - mMargins.top = top; - mMargins.right = right; - mMargins.bottom = bottom; - updatePageRects(); - } - - /** - * Sets visible page count to one or two. Should be either SHOW_ONE_PAGE or - * SHOW_TWO_PAGES. - */ - public synchronized void setViewMode(int viewmode) { - if (viewmode == SHOW_ONE_PAGE) { - mViewMode = viewmode; - updatePageRects(); - } else if (viewmode == SHOW_TWO_PAGES) { - mViewMode = viewmode; - updatePageRects(); - } - } - - /** - * Translates screen coordinates into view coordinates. - */ - public void translate(PointF pt) { - pt.x = mViewRect.left + (mViewRect.width() * pt.x / mViewportWidth); - pt.y = mViewRect.top - (-mViewRect.height() * pt.y / mViewportHeight); - } - - /** - * Recalculates page rectangles. - */ - private void updatePageRects() { - if (mViewRect.width() == 0 || mViewRect.height() == 0) { - return; - } else if (mViewMode == SHOW_ONE_PAGE) { - mPageRectRight.set(mViewRect); - mPageRectRight.left += mViewRect.width() * mMargins.left; - mPageRectRight.right -= mViewRect.width() * mMargins.right; - mPageRectRight.top += mViewRect.height() * mMargins.top; - mPageRectRight.bottom -= mViewRect.height() * mMargins.bottom; - - mPageRectLeft.set(mPageRectRight); - mPageRectLeft.offset(-mPageRectRight.width(), 0); - - int bitmapW = (int) ((mPageRectRight.width() * mViewportWidth) / mViewRect - .width()); - int bitmapH = (int) ((mPageRectRight.height() * mViewportHeight) / mViewRect - .height()); - mObserver.onPageSizeChanged(bitmapW, bitmapH); - } else if (mViewMode == SHOW_TWO_PAGES) { - mPageRectRight.set(mViewRect); - mPageRectRight.left += mViewRect.width() * mMargins.left; - mPageRectRight.right -= mViewRect.width() * mMargins.right; - mPageRectRight.top += mViewRect.height() * mMargins.top; - mPageRectRight.bottom -= mViewRect.height() * mMargins.bottom; - - mPageRectLeft.set(mPageRectRight); - mPageRectLeft.right = (mPageRectLeft.right + mPageRectLeft.left) / 2; - mPageRectRight.left = mPageRectLeft.right; - - int bitmapW = (int) ((mPageRectRight.width() * mViewportWidth) / mViewRect - .width()); - int bitmapH = (int) ((mPageRectRight.height() * mViewportHeight) / mViewRect - .height()); - mObserver.onPageSizeChanged(bitmapW, bitmapH); - } - } - - /** - * Observer for waiting render engine/state updates. - */ - public interface Observer { - /** - * Called from onDrawFrame called before rendering is started. This is - * intended to be used for animation purposes. - */ - public void onDrawFrame(); - - /** - * Called once page size is changed. Width and height tell the page size - * in pixels making it possible to update textures accordingly. - */ - public void onPageSizeChanged(int width, int height); - - /** - * Called from onSurfaceCreated to enable texture re-initialization etc - * what needs to be done when this happens. - */ - public void onSurfaceCreated(); - } -} diff --git a/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlRenderer.kt b/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlRenderer.kt new file mode 100644 index 000000000..9d7913027 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlRenderer.kt @@ -0,0 +1,252 @@ +package io.legado.app.ui.widget.page.curl + +import android.graphics.Color +import android.graphics.PointF +import android.graphics.RectF +import android.opengl.GLSurfaceView +import android.opengl.GLU +import java.util.* +import javax.microedition.khronos.egl.EGLConfig +import javax.microedition.khronos.opengles.GL10 + +/** + * Actual renderer class. + * + * @author harism + */ +class CurlRenderer +/** + * Basic constructor. + */ + (private val mObserver: CurlRenderer.Observer) : GLSurfaceView.Renderer { + // Background fill color. + private var mBackgroundColor: Int = 0 + // Curl meshes used for static and dynamic rendering. + private val mCurlMeshes: Vector = 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 (USE_PERSPECTIVE_PROJECTION) { + gl.glTranslatef(0f, 0f, -6f) + } + + 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 + updatePageRects() + + gl.glMatrixMode(GL10.GL_PROJECTION) + gl.glLoadIdentity() + if (USE_PERSPECTIVE_PROJECTION) { + GLU.gluPerspective(gl, 20f, width.toFloat() / height, .1f, 100f) + } else { + 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) + } + + /** + * Change background/clear color. + */ + fun setBackgroundColor(color: Int) { + mBackgroundColor = color + } + + /** + * Set margins or padding. Note: margins are proportional. Meaning a value + * of .1f will produce a 10% margin. + */ + @Synchronized + fun setMargins( + left: Float, top: Float, right: Float, + bottom: Float + ) { + mMargins.left = left + mMargins.top = top + mMargins.right = right + mMargins.bottom = bottom + updatePageRects() + } + + /** + * Sets visible page count to one or two. Should be either SHOW_ONE_PAGE or + * SHOW_TWO_PAGES. + */ + @Synchronized + fun setViewMode(viewmode: Int) { + if (viewmode == SHOW_ONE_PAGE) { + mViewMode = viewmode + updatePageRects() + } else if (viewmode == SHOW_TWO_PAGES) { + mViewMode = viewmode + updatePageRects() + } + } + + /** + * 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 updatePageRects() { + 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() + } + + 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 + // Set to true for checking quickly how perspective projection looks. + private const val USE_PERSPECTIVE_PROJECTION = false + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlView.java b/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlView.java deleted file mode 100644 index c2428513c..000000000 --- a/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlView.java +++ /dev/null @@ -1,790 +0,0 @@ -package io.legado.app.ui.widget.page.curl; - -import android.content.Context; -import android.graphics.PixelFormat; -import android.graphics.PointF; -import android.graphics.RectF; -import android.opengl.GLSurfaceView; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; - -/** - * OpenGL ES View. - * - * @author harism - */ -public class CurlView extends GLSurfaceView implements View.OnTouchListener, - CurlRenderer.Observer { - - // Curl state. We are flipping none, left or right page. - private static final int CURL_LEFT = 1; - private static final int CURL_NONE = 0; - private static final int CURL_RIGHT = 2; - - // Constants for mAnimationTargetEvent. - private static final int SET_CURL_TO_LEFT = 1; - private static final int SET_CURL_TO_RIGHT = 2; - - // Shows one page at the center of view. - public static final int SHOW_ONE_PAGE = 1; - // Shows two pages side by side. - public static final int SHOW_TWO_PAGES = 2; - - private boolean mAllowLastPageCurl = true; - - private boolean mAnimate = false; - private long mAnimationDurationTime = 300; - private PointF mAnimationSource = new PointF(); - private long mAnimationStartTime; - private PointF mAnimationTarget = new PointF(); - private int mAnimationTargetEvent; - - private PointF mCurlDir = new PointF(); - - private PointF mCurlPos = new PointF(); - private int mCurlState = CURL_NONE; - // Current bitmap index. This is always showed as front of right page. - private int mCurrentIndex = 0; - - // Start position for dragging. - private PointF mDragStartPos = new PointF(); - - private boolean mEnableTouchPressure = false; - // Bitmap size. These are updated from renderer once it's initialized. - private int mPageBitmapHeight = -1; - - private int mPageBitmapWidth = -1; - // Page meshes. Left and right meshes are 'static' while curl is used to - // show page flipping. - private CurlMesh mPageCurl; - - private CurlMesh mPageLeft; - private PageProvider mPageProvider; - private CurlMesh mPageRight; - - private PointerPosition mPointerPos = new PointerPosition(); - - private CurlRenderer mRenderer; - private boolean mRenderLeftPage = true; - private SizeChangedObserver mSizeChangedObserver; - - // One page is the default. - private int mViewMode = SHOW_ONE_PAGE; - - /** - * Default constructor. - */ - public CurlView(Context ctx) { - super(ctx); - init(ctx); - } - - /** - * Default constructor. - */ - public CurlView(Context ctx, AttributeSet attrs) { - super(ctx, attrs); - init(ctx); - } - - /** - * Default constructor. - */ - public CurlView(Context ctx, AttributeSet attrs, int defStyle) { - this(ctx, attrs); - } - - /** - * Get current page index. Page indices are zero based values presenting - * page being shown on right side of the book. - */ - public int getCurrentIndex() { - return mCurrentIndex; - } - - /** - * Initialize method. - */ - private void init(Context ctx) { - setEGLConfigChooser(8, 8, 8, 8, 16, 0); - getHolder().setFormat(PixelFormat.TRANSLUCENT); - setZOrderOnTop(true); - mRenderer = new CurlRenderer(this); - setRenderer(mRenderer); - setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); - setOnTouchListener(this); - - // Even though left and right pages are static we have to allocate room - // for curl on them too as we are switching meshes. Another way would be - // to swap texture ids only. - mPageLeft = new CurlMesh(10); - mPageRight = new CurlMesh(10); - mPageCurl = new CurlMesh(10); - mPageLeft.setFlipTexture(true); - mPageRight.setFlipTexture(false); - } - - @Override - public void onDrawFrame() { - // We are not animating. - if (mAnimate == false) { - return; - } - - long currentTime = System.currentTimeMillis(); - // If animation is done. - if (currentTime >= mAnimationStartTime + mAnimationDurationTime) { - if (mAnimationTargetEvent == SET_CURL_TO_RIGHT) { - // Switch curled page to right. - CurlMesh right = mPageCurl; - CurlMesh curl = mPageRight; - right.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)); - right.setFlipTexture(false); - right.reset(); - mRenderer.removeCurlMesh(curl); - mPageCurl = curl; - mPageRight = right; - // If we were curling left page update current index. - if (mCurlState == CURL_LEFT) { - --mCurrentIndex; - } - } else if (mAnimationTargetEvent == SET_CURL_TO_LEFT) { - // Switch curled page to left. - CurlMesh left = mPageCurl; - CurlMesh curl = mPageLeft; - left.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)); - left.setFlipTexture(true); - left.reset(); - mRenderer.removeCurlMesh(curl); - if (!mRenderLeftPage) { - mRenderer.removeCurlMesh(left); - } - mPageCurl = curl; - mPageLeft = left; - // If we were curling right page update current index. - if (mCurlState == CURL_RIGHT) { - ++mCurrentIndex; - } - } - mCurlState = CURL_NONE; - mAnimate = false; - requestRender(); - } else { - mPointerPos.mPos.set(mAnimationSource); - float t = 1f - ((float) (currentTime - mAnimationStartTime) / mAnimationDurationTime); - t = 1f - (t * t * t * (3 - 2 * t)); - mPointerPos.mPos.x += (mAnimationTarget.x - mAnimationSource.x) * t; - mPointerPos.mPos.y += (mAnimationTarget.y - mAnimationSource.y) * t; - updateCurlPos(mPointerPos); - } - } - - @Override - public void onPageSizeChanged(int width, int height) { - mPageBitmapWidth = width; - mPageBitmapHeight = height; - updatePages(); - requestRender(); - } - - @Override - public void onSizeChanged(int w, int h, int ow, int oh) { - super.onSizeChanged(w, h, ow, oh); - requestRender(); - if (mSizeChangedObserver != null) { - mSizeChangedObserver.onSizeChanged(w, h); - } - } - - @Override - public void onSurfaceCreated() { - // In case surface is recreated, let page meshes drop allocated texture - // ids and ask for new ones. There's no need to set textures here as - // onPageSizeChanged should be called later on. - mPageLeft.resetTexture(); - mPageRight.resetTexture(); - mPageCurl.resetTexture(); - } - - @Override - public boolean onTouch(View view, MotionEvent me) { - // No dragging during animation at the moment. - // TODO: Stop animation on touch event and return to drag mode. - if (mAnimate || mPageProvider == null) { - return false; - } - - // We need page rects quite extensively so get them for later use. - RectF rightRect = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT); - RectF leftRect = mRenderer.getPageRect(CurlRenderer.PAGE_LEFT); - - // Store pointer position. - mPointerPos.mPos.set(me.getX(), me.getY()); - mRenderer.translate(mPointerPos.mPos); - if (mEnableTouchPressure) { - mPointerPos.mPressure = me.getPressure(); - } else { - mPointerPos.mPressure = 0.8f; - } - - switch (me.getAction()) { - case MotionEvent.ACTION_DOWN: { - - // Once we receive pointer down event its position is mapped to - // right or left edge of page and that'll be the position from where - // user is holding the paper to make curl happen. - mDragStartPos.set(mPointerPos.mPos); - - // First we make sure it's not over or below page. Pages are - // supposed to be same height so it really doesn't matter do we use - // left or right one. - if (mDragStartPos.y > rightRect.top) { - mDragStartPos.y = rightRect.top; - } else if (mDragStartPos.y < rightRect.bottom) { - mDragStartPos.y = rightRect.bottom; - } - - // Then we have to make decisions for the user whether curl is going - // to happen from left or right, and on which page. - if (mViewMode == SHOW_TWO_PAGES) { - // If we have an open book and pointer is on the left from right - // page we'll mark drag position to left edge of left page. - // Additionally checking mCurrentIndex is higher than zero tells - // us there is a visible page at all. - if (mDragStartPos.x < rightRect.left && mCurrentIndex > 0) { - mDragStartPos.x = leftRect.left; - startCurl(CURL_LEFT); - } - // Otherwise check pointer is on right page's side. - else if (mDragStartPos.x >= rightRect.left - && mCurrentIndex < mPageProvider.getPageCount()) { - mDragStartPos.x = rightRect.right; - if (!mAllowLastPageCurl - && mCurrentIndex >= mPageProvider.getPageCount() - 1) { - return false; - } - startCurl(CURL_RIGHT); - } - } else if (mViewMode == SHOW_ONE_PAGE) { - float halfX = (rightRect.right + rightRect.left) / 2; - if (mDragStartPos.x < halfX && mCurrentIndex > 0) { - mDragStartPos.x = rightRect.left; - startCurl(CURL_LEFT); - } else if (mDragStartPos.x >= halfX - && mCurrentIndex < mPageProvider.getPageCount()) { - mDragStartPos.x = rightRect.right; - if (!mAllowLastPageCurl - && mCurrentIndex >= mPageProvider.getPageCount() - 1) { - return false; - } - startCurl(CURL_RIGHT); - } - } - // If we have are in curl state, let this case clause flow through - // to next one. We have pointer position and drag position defined - // and this will create first render request given these points. - if (mCurlState == CURL_NONE) { - return false; - } - } - case MotionEvent.ACTION_MOVE: { - updateCurlPos(mPointerPos); - break; - } - case MotionEvent.ACTION_CANCEL: - case MotionEvent.ACTION_UP: { - if (mCurlState == CURL_LEFT || mCurlState == CURL_RIGHT) { - // Animation source is the point from where animation starts. - // Also it's handled in a way we actually simulate touch events - // meaning the output is exactly the same as if user drags the - // page to other side. While not producing the best looking - // result (which is easier done by altering curl position and/or - // direction directly), this is done in a hope it made code a - // bit more readable and easier to maintain. - mAnimationSource.set(mPointerPos.mPos); - mAnimationStartTime = System.currentTimeMillis(); - - // Given the explanation, here we decide whether to simulate - // drag to left or right end. - if ((mViewMode == SHOW_ONE_PAGE && mPointerPos.mPos.x > (rightRect.left + rightRect.right) / 2) - || mViewMode == SHOW_TWO_PAGES - && mPointerPos.mPos.x > rightRect.left) { - // On right side target is always right page's right border. - mAnimationTarget.set(mDragStartPos); - mAnimationTarget.x = mRenderer - .getPageRect(CurlRenderer.PAGE_RIGHT).right; - mAnimationTargetEvent = SET_CURL_TO_RIGHT; - } else { - // On left side target depends on visible pages. - mAnimationTarget.set(mDragStartPos); - if (mCurlState == CURL_RIGHT || mViewMode == SHOW_TWO_PAGES) { - mAnimationTarget.x = leftRect.left; - } else { - mAnimationTarget.x = rightRect.left; - } - mAnimationTargetEvent = SET_CURL_TO_LEFT; - } - mAnimate = true; - requestRender(); - } - break; - } - } - - return true; - } - - /** - * Allow the last page to curl. - */ - public void setAllowLastPageCurl(boolean allowLastPageCurl) { - mAllowLastPageCurl = allowLastPageCurl; - } - - /** - * Sets mPageCurl curl position. - */ - private void setCurlPos(PointF curlPos, PointF curlDir, double radius) { - - // First reposition curl so that page doesn't 'rip off' from book. - if (mCurlState == CURL_RIGHT - || (mCurlState == CURL_LEFT && mViewMode == SHOW_ONE_PAGE)) { - RectF pageRect = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT); - if (curlPos.x >= pageRect.right) { - mPageCurl.reset(); - requestRender(); - return; - } - if (curlPos.x < pageRect.left) { - curlPos.x = pageRect.left; - } - if (curlDir.y != 0) { - float diffX = curlPos.x - pageRect.left; - float leftY = curlPos.y + (diffX * curlDir.x / curlDir.y); - if (curlDir.y < 0 && leftY < pageRect.top) { - curlDir.x = curlPos.y - pageRect.top; - curlDir.y = pageRect.left - curlPos.x; - } else if (curlDir.y > 0 && leftY > pageRect.bottom) { - curlDir.x = pageRect.bottom - curlPos.y; - curlDir.y = curlPos.x - pageRect.left; - } - } - } else if (mCurlState == CURL_LEFT) { - RectF pageRect = mRenderer.getPageRect(CurlRenderer.PAGE_LEFT); - if (curlPos.x <= pageRect.left) { - mPageCurl.reset(); - requestRender(); - return; - } - if (curlPos.x > pageRect.right) { - curlPos.x = pageRect.right; - } - if (curlDir.y != 0) { - float diffX = curlPos.x - pageRect.right; - float rightY = curlPos.y + (diffX * curlDir.x / curlDir.y); - if (curlDir.y < 0 && rightY < pageRect.top) { - curlDir.x = pageRect.top - curlPos.y; - curlDir.y = curlPos.x - pageRect.right; - } else if (curlDir.y > 0 && rightY > pageRect.bottom) { - curlDir.x = curlPos.y - pageRect.bottom; - curlDir.y = pageRect.right - curlPos.x; - } - } - } - - // Finally normalize direction vector and do rendering. - double dist = Math.sqrt(curlDir.x * curlDir.x + curlDir.y * curlDir.y); - if (dist != 0) { - curlDir.x /= dist; - curlDir.y /= dist; - mPageCurl.curl(curlPos, curlDir, radius); - } else { - mPageCurl.reset(); - } - - requestRender(); - } - - /** - * Set current page index. Page indices are zero based values presenting - * page being shown on right side of the book. E.g if you set value to 4; - * right side front facing bitmap will be with index 4, back facing 5 and - * for left side page index 3 is front facing, and index 2 back facing (once - * page is on left side it's flipped over). - *

- * Current index is rounded to closest value divisible with 2. - */ - public void setCurrentIndex(int index) { - if (mPageProvider == null || index < 0) { - mCurrentIndex = 0; - } else { - if (mAllowLastPageCurl) { - mCurrentIndex = Math.min(index, mPageProvider.getPageCount()); - } else { - mCurrentIndex = Math.min(index, - mPageProvider.getPageCount() - 1); - } - } - updatePages(); - requestRender(); - } - - /** - * If set to true, touch event pressure information is used to adjust curl - * radius. The more you press, the flatter the curl becomes. This is - * somewhat experimental and results may vary significantly between devices. - * On emulator pressure information seems to be flat 1.0f which is maximum - * value and therefore not very much of use. - */ - public void setEnableTouchPressure(boolean enableTouchPressure) { - mEnableTouchPressure = enableTouchPressure; - } - - /** - * Set margins (or padding). Note: margins are proportional. Meaning a value - * of .1f will produce a 10% margin. - */ - public void setMargins(float left, float top, float right, float bottom) { - mRenderer.setMargins(left, top, right, bottom); - } - - /** - * Update/set page provider. - */ - public void setPageProvider(PageProvider pageProvider) { - mPageProvider = pageProvider; - mCurrentIndex = 0; - updatePages(); - requestRender(); - } - - /** - * Setter for whether left side page is rendered. This is useful mostly for - * situations where right (main) page is aligned to left side of screen and - * left page is not visible anyway. - */ - public void setRenderLeftPage(boolean renderLeftPage) { - mRenderLeftPage = renderLeftPage; - } - - /** - * Sets SizeChangedObserver for this View. Call back method is called from - * this View's onSizeChanged method. - */ - public void setSizeChangedObserver(SizeChangedObserver observer) { - mSizeChangedObserver = observer; - } - - /** - * Sets view mode. Value can be either SHOW_ONE_PAGE or SHOW_TWO_PAGES. In - * former case right page is made size of display, and in latter case two - * pages are laid on visible area. - */ - public void setViewMode(int viewMode) { - switch (viewMode) { - case SHOW_ONE_PAGE: - mViewMode = viewMode; - mPageLeft.setFlipTexture(true); - mRenderer.setViewMode(CurlRenderer.SHOW_ONE_PAGE); - break; - case SHOW_TWO_PAGES: - mViewMode = viewMode; - mPageLeft.setFlipTexture(false); - mRenderer.setViewMode(CurlRenderer.SHOW_TWO_PAGES); - break; - } - } - - /** - * Switches meshes and loads new bitmaps if available. Updated to support 2 - * pages in landscape - */ - private void startCurl(int page) { - switch (page) { - - // Once right side page is curled, first right page is assigned into - // curled page. And if there are more bitmaps available new bitmap is - // loaded into right side mesh. - case CURL_RIGHT: { - // Remove meshes from renderer. - mRenderer.removeCurlMesh(mPageLeft); - mRenderer.removeCurlMesh(mPageRight); - mRenderer.removeCurlMesh(mPageCurl); - - // We are curling right page. - CurlMesh curl = mPageRight; - mPageRight = mPageCurl; - mPageCurl = curl; - - if (mCurrentIndex > 0) { - mPageLeft.setFlipTexture(true); - mPageLeft - .setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)); - mPageLeft.reset(); - if (mRenderLeftPage) { - mRenderer.addCurlMesh(mPageLeft); - } - } - if (mCurrentIndex < mPageProvider.getPageCount() - 1) { - updatePage(mPageRight.getTexturePage(), mCurrentIndex + 1); - mPageRight.setRect(mRenderer - .getPageRect(CurlRenderer.PAGE_RIGHT)); - mPageRight.setFlipTexture(false); - mPageRight.reset(); - mRenderer.addCurlMesh(mPageRight); - } - - // Add curled page to renderer. - mPageCurl.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)); - mPageCurl.setFlipTexture(false); - mPageCurl.reset(); - mRenderer.addCurlMesh(mPageCurl); - - mCurlState = CURL_RIGHT; - break; - } - - // On left side curl, left page is assigned to curled page. And if - // there are more bitmaps available before currentIndex, new bitmap - // is loaded into left page. - case CURL_LEFT: { - // Remove meshes from renderer. - mRenderer.removeCurlMesh(mPageLeft); - mRenderer.removeCurlMesh(mPageRight); - mRenderer.removeCurlMesh(mPageCurl); - - // We are curling left page. - CurlMesh curl = mPageLeft; - mPageLeft = mPageCurl; - mPageCurl = curl; - - if (mCurrentIndex > 1) { - updatePage(mPageLeft.getTexturePage(), mCurrentIndex - 2); - mPageLeft.setFlipTexture(true); - mPageLeft - .setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)); - mPageLeft.reset(); - if (mRenderLeftPage) { - mRenderer.addCurlMesh(mPageLeft); - } - } - - // If there is something to show on right page add it to renderer. - if (mCurrentIndex < mPageProvider.getPageCount()) { - mPageRight.setFlipTexture(false); - mPageRight.setRect(mRenderer - .getPageRect(CurlRenderer.PAGE_RIGHT)); - mPageRight.reset(); - mRenderer.addCurlMesh(mPageRight); - } - - // How dragging previous page happens depends on view mode. - if (mViewMode == SHOW_ONE_PAGE - || (mCurlState == CURL_LEFT && mViewMode == SHOW_TWO_PAGES)) { - mPageCurl.setRect(mRenderer - .getPageRect(CurlRenderer.PAGE_RIGHT)); - mPageCurl.setFlipTexture(false); - } else { - mPageCurl - .setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)); - mPageCurl.setFlipTexture(true); - } - mPageCurl.reset(); - mRenderer.addCurlMesh(mPageCurl); - - mCurlState = CURL_LEFT; - break; - } - - } - } - - /** - * Updates curl position. - */ - private void updateCurlPos(PointerPosition pointerPos) { - - // Default curl radius. - double radius = mRenderer.getPageRect(CURL_RIGHT).width() / 3; - // TODO: This is not an optimal solution. Based on feedback received so - // far; pressure is not very accurate, it may be better not to map - // coefficient to range [0f, 1f] but something like [.2f, 1f] instead. - // Leaving it as is until get my hands on a real device. On emulator - // this doesn't work anyway. - radius *= Math.max(1f - pointerPos.mPressure, 0f); - // NOTE: Here we set pointerPos to mCurlPos. It might be a bit confusing - // later to see e.g "mCurlPos.x - mDragStartPos.x" used. But it's - // actually pointerPos we are doing calculations against. Why? Simply to - // optimize code a bit with the cost of making it unreadable. Otherwise - // we had to this in both of the next if-else branches. - mCurlPos.set(pointerPos.mPos); - - // If curl happens on right page, or on left page on two page mode, - // we'll calculate curl position from pointerPos. - if (mCurlState == CURL_RIGHT - || (mCurlState == CURL_LEFT && mViewMode == SHOW_TWO_PAGES)) { - - mCurlDir.x = mCurlPos.x - mDragStartPos.x; - mCurlDir.y = mCurlPos.y - mDragStartPos.y; - float dist = (float) Math.sqrt(mCurlDir.x * mCurlDir.x + mCurlDir.y - * mCurlDir.y); - - // Adjust curl radius so that if page is dragged far enough on - // opposite side, radius gets closer to zero. - float pageWidth = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT) - .width(); - double curlLen = radius * Math.PI; - if (dist > (pageWidth * 2) - curlLen) { - curlLen = Math.max((pageWidth * 2) - dist, 0f); - radius = curlLen / Math.PI; - } - - // Actual curl position calculation. - if (dist >= curlLen) { - double translate = (dist - curlLen) / 2; - if (mViewMode == SHOW_TWO_PAGES) { - mCurlPos.x -= mCurlDir.x * translate / dist; - } else { - float pageLeftX = mRenderer - .getPageRect(CurlRenderer.PAGE_RIGHT).left; - radius = Math.max(Math.min(mCurlPos.x - pageLeftX, radius), - 0f); - } - mCurlPos.y -= mCurlDir.y * translate / dist; - } else { - double angle = Math.PI * Math.sqrt(dist / curlLen); - double translate = radius * Math.sin(angle); - mCurlPos.x += mCurlDir.x * translate / dist; - mCurlPos.y += mCurlDir.y * translate / dist; - } - } - // Otherwise we'll let curl follow pointer position. - else if (mCurlState == CURL_LEFT) { - - // Adjust radius regarding how close to page edge we are. - float pageLeftX = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT).left; - radius = Math.max(Math.min(mCurlPos.x - pageLeftX, radius), 0f); - - float pageRightX = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT).right; - mCurlPos.x -= Math.min(pageRightX - mCurlPos.x, radius); - mCurlDir.x = mCurlPos.x + mDragStartPos.x; - mCurlDir.y = mCurlPos.y - mDragStartPos.y; - } - - setCurlPos(mCurlPos, mCurlDir, radius); - } - - /** - * Updates given CurlPage via PageProvider for page located at index. - */ - private void updatePage(CurlPage page, int index) { - // First reset page to initial state. - page.reset(); - // Ask page provider to fill it up with bitmaps and colors. - mPageProvider.updatePage(page, mPageBitmapWidth, mPageBitmapHeight, - index); - } - - /** - * Updates bitmaps for page meshes. - */ - private void updatePages() { - if (mPageProvider == null || mPageBitmapWidth <= 0 - || mPageBitmapHeight <= 0) { - return; - } - - // Remove meshes from renderer. - mRenderer.removeCurlMesh(mPageLeft); - mRenderer.removeCurlMesh(mPageRight); - mRenderer.removeCurlMesh(mPageCurl); - - int leftIdx = mCurrentIndex - 1; - int rightIdx = mCurrentIndex; - int curlIdx = -1; - if (mCurlState == CURL_LEFT) { - curlIdx = leftIdx; - --leftIdx; - } else if (mCurlState == CURL_RIGHT) { - curlIdx = rightIdx; - ++rightIdx; - } - - if (rightIdx >= 0 && rightIdx < mPageProvider.getPageCount()) { - updatePage(mPageRight.getTexturePage(), rightIdx); - mPageRight.setFlipTexture(false); - mPageRight.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)); - mPageRight.reset(); - mRenderer.addCurlMesh(mPageRight); - } - if (leftIdx >= 0 && leftIdx < mPageProvider.getPageCount()) { - updatePage(mPageLeft.getTexturePage(), leftIdx); - mPageLeft.setFlipTexture(true); - mPageLeft.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)); - mPageLeft.reset(); - if (mRenderLeftPage) { - mRenderer.addCurlMesh(mPageLeft); - } - } - if (curlIdx >= 0 && curlIdx < mPageProvider.getPageCount()) { - updatePage(mPageCurl.getTexturePage(), curlIdx); - - if (mCurlState == CURL_RIGHT) { - mPageCurl.setFlipTexture(true); - mPageCurl.setRect(mRenderer - .getPageRect(CurlRenderer.PAGE_RIGHT)); - } else { - mPageCurl.setFlipTexture(false); - mPageCurl - .setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT)); - } - - mPageCurl.reset(); - mRenderer.addCurlMesh(mPageCurl); - } - } - - /** - * Provider for feeding 'book' with bitmaps which are used for rendering - * pages. - */ - public interface PageProvider { - - /** - * Return number of pages available. - */ - public int getPageCount(); - - /** - * Called once new bitmaps/textures are needed. Width and height are in - * pixels telling the size it will be drawn on screen and following them - * ensures that aspect ratio remains. But it's possible to return bitmap - * of any size though. You should use provided CurlPage for storing page - * information for requested page number.
- *
- * Index is a number between 0 and getBitmapCount() - 1. - */ - public void updatePage(CurlPage page, int width, int height, int index); - } - - /** - * Simple holder for pointer position. - */ - private class PointerPosition { - PointF mPos = new PointF(); - float mPressure; - } - - /** - * Observer interface for handling CurlView size changes. - */ - public interface SizeChangedObserver { - - /** - * Called once CurlView size changes. - */ - public void onSizeChanged(int width, int height); - } - -} diff --git a/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlView.kt b/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlView.kt new file mode 100644 index 000000000..f7dfd14f7 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/page/curl/CurlView.kt @@ -0,0 +1,768 @@ +package io.legado.app.ui.widget.page.curl + +import android.content.Context +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 { + + 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 var mPageProvider: PageProvider? = null + + 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 + + /** + * 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 { + 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 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 + } + } 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 + } + } + 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 + } + + /** + * Set margins (or padding). Note: margins are proportional. Meaning a value + * of .1f will produce a 10% margin. + */ + fun setMargins(left: Float, top: Float, right: Float, bottom: Float) { + mRenderer.setMargins(left, top, right, bottom) + } + + /** + * Update/set page provider. + */ + fun setPageProvider(pageProvider: PageProvider) { + mPageProvider = pageProvider + mCurrentIndex = 0 + updatePages() + requestRender() + } + + /** + * Setter for whether left side page is rendered. This is useful mostly for + * situations where right (main) page is aligned to left side of screen and + * left page is not visible anyway. + */ + 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. + */ + private 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.

+ *

+ * 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) + } + + 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 + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/page/delegate/PageDelegate.kt b/app/src/main/java/io/legado/app/ui/widget/page/delegate/PageDelegate.kt index a7f690890..93174923d 100644 --- a/app/src/main/java/io/legado/app/ui/widget/page/delegate/PageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/widget/page/delegate/PageDelegate.kt @@ -6,6 +6,7 @@ import android.graphics.RectF import android.view.GestureDetector import android.view.MotionEvent import android.widget.Scroller +import androidx.annotation.CallSuper import androidx.interpolator.view.animation.FastOutLinearInInterpolator import io.legado.app.ui.widget.page.ContentView import io.legado.app.ui.widget.page.PageView @@ -173,7 +174,8 @@ abstract class PageDelegate(protected val pageView: PageView) { /** * 触摸事件处理 */ - fun onTouch(event: MotionEvent): Boolean { + @CallSuper + open fun onTouch(event: MotionEvent): Boolean { if (isStarted) return false if (curPage?.isTextSelected() == true) { curPage?.dispatchTouchEvent(event) diff --git a/app/src/main/java/io/legado/app/ui/widget/page/delegate/SimulationPageDelegate.kt b/app/src/main/java/io/legado/app/ui/widget/page/delegate/SimulationPageDelegate.kt index 2020e3a60..186fc8c97 100644 --- a/app/src/main/java/io/legado/app/ui/widget/page/delegate/SimulationPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/widget/page/delegate/SimulationPageDelegate.kt @@ -1,9 +1,29 @@ package io.legado.app.ui.widget.page.delegate import android.graphics.Canvas +import android.view.MotionEvent import io.legado.app.ui.widget.page.PageView +import io.legado.app.ui.widget.page.curl.CurlPage +import io.legado.app.ui.widget.page.curl.CurlView +import io.legado.app.utils.screenshot class SimulationPageDelegate(pageView: PageView) : PageDelegate(pageView) { + + init { + pageView.curlView ?: let { + pageView.curlView = CurlView(pageView.context) + pageView.addView(pageView.curlView) + pageView.curlView?.setPageProvider(PageProvider()) + pageView.curlView?.setSizeChangedObserver(SizeChangedObserver()) + pageView.curlView?.currentIndex = 0 + } + } + + override fun onTouch(event: MotionEvent): Boolean { + pageView.curlView?.dispatchTouchEvent(event) + return super.onTouch(event) + } + override fun onScrollStart() { } @@ -12,4 +32,23 @@ class SimulationPageDelegate(pageView: PageView) : PageDelegate(pageView) { override fun onScrollStop() { } + + private inner class PageProvider : CurlView.PageProvider { + + override val pageCount: Int + get() = 1 + + override fun updatePage(page: CurlPage, width: Int, height: Int, index: Int) { + val front = curPage?.screenshot() + page.setTexture(front, CurlPage.SIDE_BOTH) + } + } + + // 定义书籍尺寸的变化监听器 + private inner class SizeChangedObserver : CurlView.SizeChangedObserver { + override fun onSizeChanged(w: Int, h: Int) { + pageView.curlView?.setViewMode(CurlView.SHOW_ONE_PAGE) + pageView.curlView?.setMargins(0f, 0f, 0f, 0f) + } + } } \ No newline at end of file