/* * Copyright 2014 Element Interactive * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package nl.elements.flipanimation; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Camera; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.widget.FrameLayout; /** * An extension of FrameLayout that allows changing the visible child using Folding animations from * page to page */ public class FoldingLayout extends FrameLayout { public static final int TOP_FRONT = 0; public static final int TOP_BACK = 1; public static final int BOTTOM_FRONT = 2; public static final int BOTTOM_BACK = 3; public static final int FOLD_DOWN=-1; public static final int FOLD_UP=1; private View[] views; private int currentView = 0; private Bitmap[] rectangles = new Bitmap[4]; private boolean folding; private long startTime; private float interpolatedTime; private Interpolator interpolator; private long duration = 500; private float deltaTime; private int nextView; private int foldingDirection=FOLD_DOWN; private int halfHeight; private int halfWidth; Paint p=new Paint(); private OnFoldListener onFoldListener; public FoldingLayout(Context context) { super(context); init(); } public FoldingLayout(Context context, AttributeSet attrs) { super(context, attrs); init(); } public FoldingLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init() { setWillNotDraw(false); invalidate(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (!folding) { views = new View[getChildCount()]; for (int i = 0; i < getChildCount(); i++) { views[i] = getChildAt(i); } showSingleChild(0); } } /** * Sets all views gone except one * * @param i The index of the view to show */ private void showSingleChild(int i) { hideAllViews(); views[i].setVisibility(VISIBLE); } /** * Sets all views as Invisible */ private void hideAllViews() { for (View view : views) { view.setVisibility(INVISIBLE); } } /** * Starts a fold */ public void fold() { if (folding) { return; } if (interpolator==null) { interpolator=new LinearInterpolator(); } nextView=(currentView+foldingDirection); if (nextView<0) { nextView=views.length-1; } if (nextView>views.length-1) { nextView=0; } if (foldingDirection==FOLD_UP) { generateRectangles(views[currentView], views[nextView]); }else { generateRectangles( views[nextView],views[currentView]); } hideAllViews(); invalidate(); folding = true; startTime = System.currentTimeMillis(); invalidate(); } /** * Reverses direction and folds */ public void foldReverse() { reverseDirection(); fold(); } /** * Folds into a determinate direction. The direction remains settled after fold * @param direction the new direction */ public void foldDirection(int direction) { setDirection(direction); fold(); } /** * Sets the direction * @param direction */ public void setDirection(int direction) { foldingDirection=direction; } /** * change the current direction, no animation is done */ public void reverseDirection() { foldingDirection=-foldingDirection; } @Override protected void onDraw(Canvas canvas) { if (folding) { halfHeight=getMeasuredHeight()/2; halfWidth=getMeasuredWidth()/2; deltaTime = System.currentTimeMillis() - startTime; interpolatedTime = interpolator.getInterpolation ((float) deltaTime / duration); if (foldingDirection==FOLD_DOWN) { interpolatedTime=1-interpolatedTime; } if (deltaTime >= duration) { stopAnimation(); }else { //Clear the canvas and draw the two static halves canvas.drawColor(0x00000000); canvas.drawBitmap(rectangles[TOP_BACK], 0, 0, null); canvas.drawBitmap(rectangles[BOTTOM_FRONT], 0, halfHeight, null); if (interpolatedTime < 0.5) { //First half of fold Matrix matrix = new Matrix(); Camera camera = new Camera(); camera.save(); camera.rotateX(-interpolatedTime * 180); camera.getMatrix(matrix); //Bitmap rotation center should be moved to 0,0,0 position before rotation matrix.preTranslate(-halfWidth, -halfHeight ); //And then put it back matrix.postTranslate(halfWidth, halfHeight ); //Apply light p.setColorFilter(applyLightness((int) (-interpolatedTime * 100))); canvas.drawBitmap(rectangles[TOP_FRONT], matrix, p); camera.restore(); } else { //Second half of fold Matrix matrix = new Matrix(); Camera camera = new Camera(); camera.save(); camera.rotateX(((1f - interpolatedTime) * 180)); camera.getMatrix(matrix); //Bitmap rotation center should be moved to 0,0,0 position before rotation matrix.preTranslate(-halfWidth, 0); //And then put it back and move it down matrix.postTranslate(halfWidth, halfHeight ); //Apply light p.setColorFilter(applyLightness((int) (interpolatedTime * 100 - 100))); canvas.drawBitmap(rectangles[BOTTOM_BACK], matrix, p); camera.restore(); } //Invoke next onDraw invalidate(); } }else{ super.onDraw(canvas); } } /** * Setup and releases all things to stop the animation */ private void stopAnimation() { folding = false; currentView=nextView; showSingleChild(currentView); invalidate(); if (onFoldListener != null) { onFoldListener.onFoldFinished(currentView, views[currentView], foldingDirection); } } /** * Generates a PorterDuff that will light or darken the applied paint * @param progress The amount of light. -100 for dark, 100 for light, 0 for normal * @return The PorterDuffColorFilter with the requested light ready to apply */ public static PorterDuffColorFilter applyLightness(int progress) { if(progress>0) { int value = (int) progress*255/100; return new PorterDuffColorFilter(Color.argb(value, 255, 255, 255), PorterDuff.Mode.SRC_OVER); } else { int value = (int) (progress*-1)*255/100; return new PorterDuffColorFilter(Color.argb(value, 0, 0, 0), PorterDuff.Mode.SRC_ATOP); } } /** * Generates the rectangles arrays with the top and bottom half of each view * * @param frontView The frontview for the animation * @param backView The backView for the animation */ private void generateRectangles(View frontView, View backView) { rectangles[TOP_FRONT] = clipBitmap(frontView, 0, frontView.getHeight() / 2); rectangles[BOTTOM_FRONT] = clipBitmap(frontView, frontView.getHeight() / 2, frontView.getHeight() ); rectangles[TOP_BACK] = clipBitmap(backView, 0, backView.getHeight() / 2); rectangles[BOTTOM_BACK] = clipBitmap(backView, backView.getHeight() / 2, backView.getHeight()); } /** * returns a new Bitmap with a clipped vertical area of the original view cache * * @param view The view to clip * @param top Top Y coordinate where to start to clip * @param bottom Lower Y coordinate where to end the clip * @return A new Bitmap with the clipped area */ private Bitmap clipBitmap(View view, int top, int bottom) { view.buildDrawingCache(false); Bitmap fullImage = view.getDrawingCache(); Bitmap clipView = Bitmap.createBitmap(fullImage.getWidth(), fullImage.getHeight() / 2, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(clipView); Rect source = new Rect(0, top, fullImage.getWidth(), bottom); Rect target = new Rect(0, 0, clipView.getWidth(), clipView.getHeight()); c.drawBitmap(fullImage, source, target, null); view.destroyDrawingCache(); return clipView; } public interface OnFoldListener{ public void onFoldFinished(int position, View view, int direction); } /** * How long this animation should last * @return the duration in milliseconds of the animation */ public long getDuration() { return duration; } /** * How long this animation should last. The duration cannot be negative. * @param duration Duration in milliseconds */ public void setDuration(long duration) { this.duration = duration; } /** * Sets the acceleration curve for this animation. Defaults to a linear interpolation. * @param interpolator The interpolator which defines the acceleration curve */ public void setInterpolator(Interpolator interpolator) { this.interpolator = interpolator; } /** * Gets the acceleration curve type for this animation. * @return the Interpolator associated to this animation */ public Interpolator getInterpolator() { return interpolator; } /** * The listener of the fold process ending * @return The current OnFoldListener object assigned */ public OnFoldListener getOnFoldListener() { return onFoldListener; } /** * Who will be notified once the folding is finished * @param onFoldListener Any object implementing OnFoldListener interface */ public void setOnFoldListener(OnFoldListener onFoldListener) { this.onFoldListener = onFoldListener; } }