/* * Copyright (C) 2011 Alex Kuiper * * This file is part of PageTurner * * PageTurner is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * PageTurner is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PageTurner. If not, see <http://www.gnu.org/licenses/>.* */ package net.nightwhistler.pageturner.animation; import android.graphics.*; import android.graphics.Paint.Style; import android.text.TextPaint; /** * PageCurl animator, simulates flipping pages. * * All credit for this goes to Moritz 'Moss' Wundke (b.thax.dcg@gmail.com), * since it's just an adapted and simplified version of his PageCurlView. * * The original project is here: http://code.google.com/p/android-page-curl/ * * @author Alex Kuiper * */ public class PageCurlAnimator implements Animator { // Debug text paint stuff private Paint mTextPaint; private TextPaint mTextPaintShadow; /** Px / Draw call */ private int mCurlSpeed; /** Fixed update time used to create a smooth curl animation */ //private int mUpdateRate; /** The initial offset for x and y axis movements */ private int mInitialEdgeOffset; /** Maximum radius a page can be flipped, by default it's the width of the view */ private float mFlipRadius; /** Point used to move */ private Vector2D mMovement; /** Movement point form the last frame */ private Vector2D mOldMovement; /** Page curl edge */ private Paint mCurlEdgePaint; /** Our points used to define the current clipping paths in our draw call */ private Vector2D mA, mB, mC, mD, mE, mF, mOrigin; /** If false no draw call has been done */ private boolean bViewDrawn; /** Defines the flip direction that is currently considered */ private boolean bFlipRight; /** LAGACY The current foreground */ private Bitmap mForeground; /** LAGACY The current background */ private Bitmap mBackground; private int backgroundColor = Color.WHITE; private int edgeColor = Color.BLACK; private boolean started = false; private boolean finished = false; private boolean drawDebugEnabled = false; public PageCurlAnimator(boolean flipRight) { this.bFlipRight = flipRight; //this.drawDebugEnabled = true; init(); } public void setDrawDebugEnabled(boolean drawDebugEnabled) { this.drawDebugEnabled = drawDebugEnabled; } @Override public synchronized void advanceOneFrame() { started = true; if ( finished || mOrigin == null ) { return; } int width = getWidth(); // Handle speed float curlSpeed = mCurlSpeed; if ( !bFlipRight ) { curlSpeed *= -1; } // Move us mMovement.x += curlSpeed; mMovement = CapMovement(mMovement, false); // Create values doSimpleCurl(); // Check for endings :D if (mA.x < 1 || mA.x > width - 1) { finished = true; ResetClipEdge(); // Create values doSimpleCurl(); } } public void setBackgroundColor(int backgroundColor) { this.backgroundColor = backgroundColor; } public void setEdgeColor( int edgeColor ) { this.edgeColor = edgeColor; } private void drawDebug(Canvas canvas) { float posX = 10; float posY = 20; Paint paint = new Paint(); paint.setStrokeWidth(5); paint.setStyle(Style.FILL); paint.setColor(Color.BLACK); canvas.drawCircle(mOrigin.x, mOrigin.y, getWidth(), paint); paint.setStrokeWidth(3); paint.setColor(Color.RED); canvas.drawCircle(mOrigin.x, mOrigin.y, getWidth(), paint); paint.setStrokeWidth(5); paint.setColor(Color.BLACK); canvas.drawLine(mOrigin.x, mOrigin.y, mMovement.x, mMovement.y, paint); paint.setStrokeWidth(3); paint.setColor(Color.RED); canvas.drawLine(mOrigin.x, mOrigin.y, mMovement.x, mMovement.y, paint); posY = debugDrawPoint(canvas,"A",mA,Color.RED,posX,posY); posY = debugDrawPoint(canvas,"B",mB,Color.GREEN,posX,posY); posY = debugDrawPoint(canvas,"C",mC,Color.BLUE,posX,posY); posY = debugDrawPoint(canvas,"D",mD,Color.CYAN,posX,posY); posY = debugDrawPoint(canvas,"E",mE,Color.YELLOW,posX,posY); posY = debugDrawPoint(canvas,"F",mF,Color.LTGRAY,posX,posY); posY = debugDrawPoint(canvas,"Mov",mMovement,Color.DKGRAY,posX,posY); posY = debugDrawPoint(canvas,"Origin",mOrigin,Color.MAGENTA,posX,posY); /**/ } private float debugDrawPoint(Canvas canvas, String name, Vector2D point, int color, float posX, float posY) { return debugDrawPoint(canvas,name+" "+point.toString(),point.x, point.y, color, posX, posY); } private float debugDrawPoint(Canvas canvas, String name, float X, float Y, int color, float posX, float posY) { mTextPaint.setColor(color); drawTextShadowed(canvas,name,posX , posY, mTextPaint,mTextPaintShadow); Paint paint = new Paint(); paint.setStrokeWidth(5); paint.setColor(color); canvas.drawPoint(X, Y, paint); return posY+15; } /** * Draw a text with a nice shadow */ public static void drawTextShadowed(Canvas canvas, String text, float x, float y, Paint textPain, Paint shadowPaint) { canvas.drawText(text, x-1, y, shadowPaint); canvas.drawText(text, x, y+1, shadowPaint); canvas.drawText(text, x+1, y, shadowPaint); canvas.drawText(text, x, y-1, shadowPaint); canvas.drawText(text, x, y, textPain); } public void setForegroundBitmap(Bitmap bitmap ) { this.mForeground = bitmap; } public void setBackgroundBitmap(Bitmap bitmap) { this.mBackground = bitmap; } private int getWidth() { if ( mBackground != null ) { return mBackground.getWidth(); } return 0; } private int getHeight() { if ( mBackground != null ) { return mBackground.getHeight(); } return 0; } @Override public void draw(Canvas canvas) { // We need to initialize all size data when we first draw the view if ( !bViewDrawn ) { bViewDrawn = true; onFirstDrawEvent(canvas); } canvas.drawColor(backgroundColor); // TODO: This just scales the views to the current // width and height. We should add some logic for: // 1) Maintain aspect ratio // 2) Uniform scale // 3) ... Rect rect = new Rect(); rect.left = 0; rect.top = 0; rect.bottom = getHeight(); rect.right = getWidth(); // First Page render Paint paint = new Paint(); // Draw our elements try { drawForeground(canvas, rect, paint); } catch (Exception e ) { //ErrorReporter.getInstance().handleException(e); } try { drawBackground(canvas, rect, paint); } catch (Exception e) { //ErrorReporter.getInstance().handleException(e); } drawCurlEdge(canvas); if ( this.drawDebugEnabled ) { drawDebug(canvas); } } /** * Initialize the view */ private final void init() { // Foreground text paint mTextPaint = new Paint(); mTextPaint.setAntiAlias(true); mTextPaint.setTextSize(16); mTextPaint.setColor(0xFF000000); // The shadow mTextPaintShadow = new TextPaint(); mTextPaintShadow.setAntiAlias(true); mTextPaintShadow.setTextSize(16); mTextPaintShadow.setColor(0x00000000); // Base padding //setPadding(3, 3, 3, 3); mMovement = new Vector2D(0,0); mOldMovement = new Vector2D(0,0); // Create our edge paint mCurlEdgePaint = new Paint(); mCurlEdgePaint.setAntiAlias(true); mCurlEdgePaint.setStyle(Paint.Style.STROKE); // mCurlEdgePaint.setColor(this.edgeColor); // mCurlEdgePaint.setShadowLayer(10, -5, 5, 0x99000000); // Set the default props, those come from an XML :D mCurlSpeed = 30; mInitialEdgeOffset = 20; } /** * Reset points to it's initial clip edge state */ public void ResetClipEdge() { // Set our base movement mMovement.x = mInitialEdgeOffset; mMovement.y = mInitialEdgeOffset; mOldMovement.x = 0; mOldMovement.y = 0; if ( ! bFlipRight ) { mMovement.x = getWidth(); mMovement.y = mInitialEdgeOffset; } // Now set the points // TODO: OK, those points MUST come from our measures and // the actual bounds of the view! mA = new Vector2D(mInitialEdgeOffset, 0); mB = new Vector2D(this.getWidth(), this.getHeight()); mC = new Vector2D(this.getWidth(), 0); mD = new Vector2D(0, 0); mE = new Vector2D(0, 0); mF = new Vector2D(0, 0); // The movement origin point mOrigin = new Vector2D(this.getWidth(), 0); } /** * Set the curl speed. * @param curlSpeed - New speed in px/frame * @throws IllegalArgumentException if curlspeed < 1 */ public void SetCurlSpeed(int curlSpeed) { if ( curlSpeed < 1 ) throw new IllegalArgumentException("curlSpeed must be greated than 0"); mCurlSpeed = curlSpeed; } /** * Get the current curl speed * @return int - Curl speed in px/frame */ public int GetCurlSpeed() { return mCurlSpeed; } /** * Set the initial pixel offset for the curl edge * @param initialEdgeOffset - px offset for curl edge * @throws IllegalArgumentException if initialEdgeOffset < 0 */ public void SetInitialEdgeOffset(int initialEdgeOffset) { if ( initialEdgeOffset < 0 ) throw new IllegalArgumentException("initialEdgeOffset can not negative"); mInitialEdgeOffset = initialEdgeOffset; } /** * Get the initial pixel offset for the curl edge * @return int - px */ public int GetInitialEdgeOffset() { return mInitialEdgeOffset; } /** * Make sure we never move too much, and make sure that if we * move too much to add a displacement so that the movement will * be still in our radius. * @param radius - radius form the flip origin * @param bMaintainMoveDir - Cap movement but do not change the * current movement direction * @return Corrected point */ private Vector2D CapMovement(Vector2D point, boolean bMaintainMoveDir) { // Make sure we never ever move too much if (point.distance(mOrigin) > mFlipRadius) { if ( bMaintainMoveDir ) { // Maintain the direction point = mOrigin.sum(point.sub(mOrigin).normalize().mult(mFlipRadius)); } else { // Change direction if ( point.x > (mOrigin.x+mFlipRadius)) point.x = (mOrigin.x+mFlipRadius); else if ( point.x < (mOrigin.x-mFlipRadius) ) point.x = (mOrigin.x-mFlipRadius); point.y = (float) (Math.sin(Math.acos(Math.abs(point.x-mOrigin.x)/mFlipRadius))*mFlipRadius); } } return point; } /** * Do a simple page curl effect */ private void doSimpleCurl() { int width = getWidth(); int height = getHeight(); // Calculate point A mA.x = width - mMovement.x; mA.y = height; // Calculate point D mD.x = 0; mD.y = 0; if (mA.x > width / 2) { mD.x = width; mD.y = height - (width - mA.x) * height / mA.x; } else { mD.x = 2 * mA.x; mD.y = 0; } // Now calculate E and F taking into account that the line // AD is perpendicular to FB and EC. B and C are fixed points. double angle = Math.atan((height - mD.y) / (mD.x + mMovement.x - width)); double _cos = Math.cos(2 * angle); double _sin = Math.sin(2 * angle); // And get F mF.x = (float) (width - mMovement.x + _cos * mMovement.x); mF.y = (float) (height - _sin * mMovement.x); // If the x position of A is above half of the page we are still not // folding the upper-right edge and so E and D are equal. if (mA.x > width / 2) { mE.x = mD.x; mE.y = mD.y; } else { // So get E mE.x = (float) (mD.x + _cos * (width - mD.x)); mE.y = (float) -(_sin * (width - mD.x)); } } /** * Called on the first draw event of the view * @param canvas */ protected void onFirstDrawEvent(Canvas canvas) { mFlipRadius = getWidth(); ResetClipEdge(); doSimpleCurl(); } /** * Draw the foreground * @param canvas * @param rect * @param paint */ private void drawForeground( Canvas canvas, Rect rect, Paint paint ) { if ( ! finished || !bFlipRight ) { if ( ! mForeground.isRecycled() ) { canvas.drawBitmap(mForeground, null, rect, null); } } } /** * Create a Path used as a mask to draw the background page * @return */ private Path createBackgroundPath() { Path path = new Path(); path.moveTo(mA.x, mA.y); path.lineTo(mB.x, mB.y); path.lineTo(mC.x, mC.y); path.lineTo(mD.x, mD.y); path.lineTo(mA.x, mA.y); return path; } /** * Draw the background image. * @param canvas * @param rect * @param paint */ private void drawBackground( Canvas canvas, Rect rect, Paint paint ) { if ( ! finished ) { Path mask = createBackgroundPath(); // Save current canvas so we do not mess it up canvas.save(); canvas.clipPath(mask); } if ( ! (finished && !bFlipRight) ) { if ( ! mBackground.isRecycled() ) { canvas.drawBitmap(mBackground, null, rect, paint); } canvas.restore(); } } /** * Creates a path used to draw the curl edge in. * @return */ private Path createCurlEdgePath() { Path path = new Path(); path.moveTo(mA.x, mA.y); path.lineTo(mD.x, mD.y); path.lineTo(mE.x, mE.y); path.lineTo(mF.x, mF.y); path.lineTo(mA.x, mA.y); return path; } /** * Draw the curl page edge * @param canvas */ private void drawCurlEdge( Canvas canvas ) { if ( started && ! finished ) { Path path = createCurlEdgePath(); mCurlEdgePaint.setColor(backgroundColor); mCurlEdgePaint.setStyle(Style.FILL); mCurlEdgePaint.setShadowLayer(10, -5, 5, edgeColor ); canvas.drawPath(path, mCurlEdgePaint); mCurlEdgePaint.setColor(edgeColor); mCurlEdgePaint.setStyle(Style.STROKE); mCurlEdgePaint.setShadowLayer(0, 0, 0, edgeColor ); canvas.drawPath(path, mCurlEdgePaint); } } @Override public int getAnimationSpeed() { return 30; } @Override public boolean isFinished() { return finished; } /** * Inner class used to represent a 2D point. */ private class Vector2D { public float x,y; public Vector2D(float x, float y) { this.x = x; this.y = y; } @Override public String toString() { // TODO Auto-generated method stub return "("+this.x+","+this.y+")"; } public boolean equals(Object o) { if (o instanceof Vector2D) { Vector2D p = (Vector2D) o; return p.x == x && p.y == y; } return false; } public Vector2D sum(Vector2D b) { return new Vector2D(x+b.x,y+b.y); } public Vector2D sub(Vector2D b) { return new Vector2D(x-b.x,y-b.y); } public float distanceSquared(Vector2D other) { float dx = other.x - x; float dy = other.y - y; return (dx * dx) + (dy * dy); } public float distance(Vector2D other) { return (float) Math.sqrt(distanceSquared(other)); } public float dotProduct(Vector2D other) { return other.x * x + other.y * y; } public Vector2D normalize() { float magnitude = (float) Math.sqrt(dotProduct(this)); return new Vector2D(x / magnitude, y / magnitude); } public Vector2D mult(float scalar) { return new Vector2D(x*scalar,y*scalar); } } @Override public void stop() { this.finished = true; } }