pull/37/head
kunfei 5 years ago
parent 5d1fab40c8
commit 2b3a9f8ff4
  1. 6
      app/src/main/java/io/legado/app/ui/widget/page/PageView.kt
  2. 16
      app/src/main/java/io/legado/app/ui/widget/page/curl/CurlMesh.java
  3. 195
      app/src/main/java/io/legado/app/ui/widget/page/curl/CurlPage.java
  4. 191
      app/src/main/java/io/legado/app/ui/widget/page/curl/CurlPage.kt
  5. 250
      app/src/main/java/io/legado/app/ui/widget/page/curl/CurlRenderer.java
  6. 252
      app/src/main/java/io/legado/app/ui/widget/page/curl/CurlRenderer.kt
  7. 790
      app/src/main/java/io/legado/app/ui/widget/page/curl/CurlView.java
  8. 768
      app/src/main/java/io/legado/app/ui/widget/page/curl/CurlView.kt
  9. 4
      app/src/main/java/io/legado/app/ui/widget/page/delegate/PageDelegate.kt
  10. 39
      app/src/main/java/io/legado/app/ui/widget/page/delegate/SimulationPageDelegate.kt

@ -7,6 +7,7 @@ import android.util.AttributeSet
import android.view.MotionEvent import android.view.MotionEvent
import android.widget.FrameLayout import android.widget.FrameLayout
import io.legado.app.help.ReadBookConfig 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.ui.widget.page.delegate.*
import io.legado.app.utils.activity import io.legado.app.utils.activity
import io.legado.app.utils.getPrefInt import io.legado.app.utils.getPrefInt
@ -23,6 +24,7 @@ class PageView(context: Context, attrs: AttributeSet) :
var prevPage: ContentView? = null var prevPage: ContentView? = null
var curPage: ContentView? = null var curPage: ContentView? = null
var nextPage: ContentView? = null var nextPage: ContentView? = null
var curlView: CurlView? = null
init { init {
callback = activity as? CallBack callback = activity as? CallBack
@ -85,6 +87,10 @@ class PageView(context: Context, attrs: AttributeSet) :
} }
fun upPageAnim() { fun upPageAnim() {
if (curlView != null) {
removeView(curlView)
curlView = null
}
pageDelegate = null pageDelegate = null
pageDelegate = when (context.getPrefInt("pageAnim")) { pageDelegate = when (context.getPrefInt("pageAnim")) {
0 -> CoverPageDelegate(this) 0 -> CoverPageDelegate(this)

@ -95,21 +95,21 @@ public class CurlMesh {
// There really is no use for 0 splits. // There really is no use for 0 splits.
mMaxCurlSplits = maxCurlSplits < 1 ? 1 : maxCurlSplits; mMaxCurlSplits = maxCurlSplits < 1 ? 1 : maxCurlSplits;
mArrScanLines = new Array<Double>(maxCurlSplits + 2); mArrScanLines = new Array<>(maxCurlSplits + 2);
mArrOutputVertices = new Array<Vertex>(7); mArrOutputVertices = new Array<>(7);
mArrRotatedVertices = new Array<Vertex>(4); mArrRotatedVertices = new Array<>(4);
mArrIntersections = new Array<Vertex>(2); mArrIntersections = new Array<>(2);
mArrTempVertices = new Array<Vertex>(7 + 4); mArrTempVertices = new Array<>(7 + 4);
for (int i = 0; i < 7 + 4; ++i) { for (int i = 0; i < 7 + 4; ++i) {
mArrTempVertices.add(new Vertex()); mArrTempVertices.add(new Vertex());
} }
if (DRAW_SHADOW) { if (DRAW_SHADOW) {
mArrSelfShadowVertices = new Array<ShadowVertex>( mArrSelfShadowVertices = new Array<>(
(mMaxCurlSplits + 2) * 2); (mMaxCurlSplits + 2) * 2);
mArrDropShadowVertices = new Array<ShadowVertex>( mArrDropShadowVertices = new Array<>(
(mMaxCurlSplits + 2) * 2); (mMaxCurlSplits + 2) * 2);
mArrTempShadowVertices = new Array<ShadowVertex>( mArrTempShadowVertices = new Array<>(
(mMaxCurlSplits + 2) * 2); (mMaxCurlSplits + 2) * 2);
for (int i = 0; i < (mMaxCurlSplits + 2) * 2; ++i) { for (int i = 0; i < (mMaxCurlSplits + 2) * 2; ++i) {
mArrTempShadowVertices.add(new ShadowVertex()); mArrTempShadowVertices.add(new ShadowVertex());

@ -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;
}
}

@ -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
}
}

@ -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<CurlMesh> 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<CurlMesh>();
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();
}
}

@ -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<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 (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
}
}

@ -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).
* <p>
* 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.<br/>
* <br/>
* 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);
}
}

@ -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.<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)
}
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
}
}

@ -6,6 +6,7 @@ import android.graphics.RectF
import android.view.GestureDetector import android.view.GestureDetector
import android.view.MotionEvent import android.view.MotionEvent
import android.widget.Scroller import android.widget.Scroller
import androidx.annotation.CallSuper
import androidx.interpolator.view.animation.FastOutLinearInInterpolator import androidx.interpolator.view.animation.FastOutLinearInInterpolator
import io.legado.app.ui.widget.page.ContentView import io.legado.app.ui.widget.page.ContentView
import io.legado.app.ui.widget.page.PageView 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 (isStarted) return false
if (curPage?.isTextSelected() == true) { if (curPage?.isTextSelected() == true) {
curPage?.dispatchTouchEvent(event) curPage?.dispatchTouchEvent(event)

@ -1,9 +1,29 @@
package io.legado.app.ui.widget.page.delegate package io.legado.app.ui.widget.page.delegate
import android.graphics.Canvas import android.graphics.Canvas
import android.view.MotionEvent
import io.legado.app.ui.widget.page.PageView 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) { 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() { override fun onScrollStart() {
} }
@ -12,4 +32,23 @@ class SimulationPageDelegate(pageView: PageView) : PageDelegate(pageView) {
override fun onScrollStop() { 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)
}
}
} }
Loading…
Cancel
Save