/*
* Copyright (C) 2013 Priboi Tiberiu
* Copyright (C) 2013 The Android Open Source Project
*
* 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 com.marshalchen.common.uimodule.foldingLayout;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.*;
import android.graphics.Paint.Style;
import android.graphics.Shader.TileMode;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import com.marshalchen.common.uimodule.R;
import com.marshalchen.common.uimodule.foldingLayout.listener.OnFoldListener;
/**
* The folding layout where the number of folds, the anchor point and the
* orientation of the fold can be specified. Each of these parameters can be
* modified individually and updates and resets the fold to a default (unfolded)
* state. The fold factor varies between 0 (completely unfolded flat image) to
* 1.0 (completely folded, non-visible image).
* <p/>
* This layout throws an exception if there is more than one child added to the
* view. For more complicated view hierarchy's inside the folding layout, the
* views should all be nested inside 1 parent layout.
* <p/>
* This layout folds the contents of its child in real time. By applying matrix
* transformations when drawing to canvas, the contents of the child may change
* as the fold takes place. It is important to note that there are jagged edges
* about the perimeter of the layout as a result of applying transformations to
* a rectangle. This can be avoided by having the child of this layout wrap its
* content inside a 1 pixel transparent border. This will cause an anti-aliasing
* like effect and smoothen out the edges.
*/
public class BaseFoldingLayout extends ViewGroup {
/*
* A bug was introduced in Android 4.3 that ignores changes to the Canvas
* state between multiple calls to super.dispatchDraw() when running with
* hardware acceleration. To account for this bug, a slightly different
* approach was taken to fold a static image whereby a bitmap of the
* original contents is captured and drawn in segments onto the canvas.
* However, this method does not permit the folding of a TextureView hosting
* a live camera feed which continuously updates. Furthermore, the sepia
* effect was removed from the bitmap variation of the demo to simplify the
* logic when running with this workaround."
*/
public static enum Orientation {
VERTICAL, HORIZONTAL
}
private final String FOLDING_VIEW_EXCEPTION_MESSAGE = "Folding Layout can only 1 child at "
+ "most";
private final float SHADING_ALPHA = 0.8f;
private final float SHADING_FACTOR = 0.5f;
private final int DEPTH_CONSTANT = 1500;
private final int NUM_OF_POLY_POINTS = 8;
private Rect[] mFoldRectArray;
private Matrix[] mMatrix;
protected Orientation mOrientation = Orientation.HORIZONTAL;
protected float mAnchorFactor = 0;
private float mFoldFactor = 0;
private int mNumberOfFolds = 2;
private boolean mIsHorizontal = true;
private int mOriginalWidth = 0;
private int mOriginalHeight = 0;
private float mFoldMaxWidth = 0;
private float mFoldMaxHeight = 0;
private float mFoldDrawWidth = 0;
private float mFoldDrawHeight = 0;
private boolean mIsFoldPrepared = false;
private boolean mShouldDraw = true;
private Paint mSolidShadow;
private Paint mGradientShadow;
private LinearGradient mShadowLinearGradient;
private Matrix mShadowGradientMatrix;
private float[] mSrc;
private float[] mDst;
private OnFoldListener mFoldListener;
private float mPreviousFoldFactor = 0;
private Bitmap mFullBitmap;
private Rect mDstRect;
public BaseFoldingLayout(Context context) {
super(context);
}
public BaseFoldingLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public BaseFoldingLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
public void init(Context context, AttributeSet attrs) {
// now style everything!
TypedArray ta = context.obtainStyledAttributes(attrs,
R.styleable.FoldingMenu);
int mFoldNumber = ta.getInt(R.styleable.FoldingMenu_foldNumber,
mNumberOfFolds);
if (mFoldNumber > 0 && mFoldNumber < 7) {
mNumberOfFolds = mFoldNumber;
} else {
mNumberOfFolds = 2;
}
ta.recycle();
}
@Override
protected boolean addViewInLayout(View child, int index,
LayoutParams params, boolean preventRequestLayout) {
throwCustomException(getChildCount());
boolean returnValue = super.addViewInLayout(child, index, params,
preventRequestLayout);
return returnValue;
}
@Override
public void addView(View child, int index, LayoutParams params) {
throwCustomException(getChildCount());
super.addView(child, index, params);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
View child = getChildAt(0);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
View child = getChildAt(0);
child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
updateFold();
}
/**
* The custom exception to be thrown so as to limit the number of views in
* this layout to at most one.
*/
private class NumberOfFoldingLayoutChildrenException extends
RuntimeException {
/**
*
*/
private static final long serialVersionUID = 1L;
public NumberOfFoldingLayoutChildrenException(String message) {
super(message);
}
}
/**
* Throws an exception if the number of views added to this layout exceeds
* one.
*/
private void throwCustomException(int numOfChildViews) {
if (numOfChildViews == 1) {
throw new NumberOfFoldingLayoutChildrenException(
FOLDING_VIEW_EXCEPTION_MESSAGE);
}
}
public void setFoldListener(OnFoldListener foldListener) {
mFoldListener = foldListener;
}
/**
* Sets the fold factor of the folding view and updates all the
* corresponding matrices and values to account for the new fold factor.
* Once that is complete, it redraws itself with the new fold.
*/
public void setFoldFactor(float foldFactor) {
if (foldFactor != mFoldFactor) {
mFoldFactor = foldFactor;
calculateMatrices();
invalidate();
}
}
public void setOrientation(Orientation orientation) {
if (orientation != mOrientation) {
mOrientation = orientation;
updateFold();
}
}
public void setAnchorFactor(float anchorFactor) {
if (anchorFactor != mAnchorFactor) {
mAnchorFactor = anchorFactor;
updateFold();
}
}
public void setNumberOfFolds(int numberOfFolds) {
if (numberOfFolds != mNumberOfFolds) {
mNumberOfFolds = numberOfFolds;
updateFold();
}
}
public float getAnchorFactor() {
return mAnchorFactor;
}
public Orientation getOrientation() {
return mOrientation;
}
public float getFoldFactor() {
return mFoldFactor;
}
public int getNumberOfFolds() {
return mNumberOfFolds;
}
private void updateFold() {
prepareFold(mOrientation, mAnchorFactor, mNumberOfFolds);
calculateMatrices();
invalidate();
}
/**
* This method is called in order to update the fold's orientation, anchor
* point and number of folds. This creates the necessary setup in order to
* prepare the layout for a fold with the specified parameters. Some of the
* dimensions required for the folding transformation are also acquired
* here.
* <p/>
* After this method is called, it will be in a completely unfolded state by
* default.
*/
private void prepareFold(Orientation orientation, float anchorFactor,
int numberOfFolds) {
mSrc = new float[NUM_OF_POLY_POINTS];
mDst = new float[NUM_OF_POLY_POINTS];
mDstRect = new Rect();
mFoldFactor = 0;
mPreviousFoldFactor = 0;
mIsFoldPrepared = false;
mSolidShadow = new Paint();
mGradientShadow = new Paint();
mOrientation = orientation;
mIsHorizontal = (orientation == Orientation.HORIZONTAL);
if (mIsHorizontal) {
mShadowLinearGradient = new LinearGradient(0, 0, SHADING_FACTOR, 0,
Color.BLACK, Color.TRANSPARENT, TileMode.CLAMP);
} else {
mShadowLinearGradient = new LinearGradient(0, 0, 0, SHADING_FACTOR,
Color.BLACK, Color.TRANSPARENT, TileMode.CLAMP);
}
mGradientShadow.setStyle(Style.FILL);
mGradientShadow.setShader(mShadowLinearGradient);
mShadowGradientMatrix = new Matrix();
mAnchorFactor = anchorFactor;
mNumberOfFolds = numberOfFolds;
mOriginalWidth = getMeasuredWidth();
mOriginalHeight = getMeasuredHeight();
mFoldRectArray = new Rect[mNumberOfFolds];
mMatrix = new Matrix[mNumberOfFolds];
for (int x = 0; x < mNumberOfFolds; x++) {
mMatrix[x] = new Matrix();
}
int h = mOriginalHeight;
int w = mOriginalWidth;
if (Util.IS_JBMR2 && h != 0 && w != 0) {
mFullBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(mFullBitmap);
getChildAt(0).draw(canvas);
}
int delta = Math.round(mIsHorizontal ? ((float) w)
/ ((float) mNumberOfFolds) : ((float) h)
/ ((float) mNumberOfFolds));
/*
* Loops through the number of folds and segments the full layout into a
* number of smaller equal components. If the number of folds is odd,
* then one of the components will be smaller than all the rest. Note
* that deltap below handles the calculation for an odd number of folds.
*/
for (int x = 0; x < mNumberOfFolds; x++) {
if (mIsHorizontal) {
int deltap = (x + 1) * delta > w ? w - x * delta : delta;
mFoldRectArray[x] = new Rect(x * delta, 0, x * delta + deltap,
h);
} else {
int deltap = (x + 1) * delta > h ? h - x * delta : delta;
mFoldRectArray[x] = new Rect(0, x * delta, w, x * delta
+ deltap);
}
}
if (mIsHorizontal) {
mFoldMaxHeight = h;
mFoldMaxWidth = delta;
} else {
mFoldMaxHeight = delta;
mFoldMaxWidth = w;
}
mIsFoldPrepared = true;
}
/*
* Calculates the transformation matrices used to draw each of the separate
* folding segments from this view.
*/
private void calculateMatrices() {
mShouldDraw = true;
if (!mIsFoldPrepared) {
return;
}
/**
* If the fold factor is 1 than the folding view should not be seen and
* the canvas can be left completely empty.
*/
if (mFoldFactor == 1) {
mShouldDraw = false;
return;
}
if (mFoldFactor == 0 && mPreviousFoldFactor > 0
&& mFoldListener != null) {
mFoldListener.onEndFold();
}
if (mPreviousFoldFactor == 0 && mFoldFactor > 0
&& mFoldListener != null) {
mFoldListener.onStartFold();
}
mPreviousFoldFactor = mFoldFactor;
/*
* Reset all the transformation matrices back to identity before
* computing the new transformation
*/
for (int x = 0; x < mNumberOfFolds; x++) {
mMatrix[x].reset();
}
float cTranslationFactor = 1 - mFoldFactor;
float translatedDistance = mIsHorizontal ? mOriginalWidth
* cTranslationFactor : mOriginalHeight * cTranslationFactor;
float translatedDistancePerFold = Math.round(translatedDistance
/ mNumberOfFolds);
/*
* For an odd number of folds, the rounding error may cause the
* translatedDistancePerFold to be grater than the max fold width or
* height.
*/
mFoldDrawWidth = mFoldMaxWidth < translatedDistancePerFold ? translatedDistancePerFold
: mFoldMaxWidth;
mFoldDrawHeight = mFoldMaxHeight < translatedDistancePerFold ? translatedDistancePerFold
: mFoldMaxHeight;
float translatedDistanceFoldSquared = translatedDistancePerFold
* translatedDistancePerFold;
/*
* Calculate the depth of the fold into the screen using pythagorean
* theorem.
*/
float depth = mIsHorizontal ? (float) Math
.sqrt((double) (mFoldDrawWidth * mFoldDrawWidth - translatedDistanceFoldSquared))
: (float) Math
.sqrt((double) (mFoldDrawHeight * mFoldDrawHeight - translatedDistanceFoldSquared));
/*
* The size of some object is always inversely proportional to the
* distance it is away from the viewpoint. The constant can be varied to
* to affect the amount of perspective.
*/
float scaleFactor = DEPTH_CONSTANT / (DEPTH_CONSTANT + depth);
float scaledWidth, scaledHeight, bottomScaledPoint, topScaledPoint, rightScaledPoint, leftScaledPoint;
if (mIsHorizontal) {
scaledWidth = mFoldDrawWidth * cTranslationFactor;
scaledHeight = mFoldDrawHeight * scaleFactor;
} else {
scaledWidth = mFoldDrawWidth * scaleFactor;
scaledHeight = mFoldDrawHeight * cTranslationFactor;
}
topScaledPoint = (mFoldDrawHeight - scaledHeight) / 2.0f;
bottomScaledPoint = topScaledPoint + scaledHeight;
leftScaledPoint = (mFoldDrawWidth - scaledWidth) / 2.0f;
rightScaledPoint = leftScaledPoint + scaledWidth;
float anchorPoint = mIsHorizontal ? mAnchorFactor * mOriginalWidth
: mAnchorFactor * mOriginalHeight;
/* The fold along which the anchor point is located. */
float midFold = mIsHorizontal ? (anchorPoint / mFoldDrawWidth)
: anchorPoint / mFoldDrawHeight;
mSrc[0] = 0;
mSrc[1] = 0;
mSrc[2] = 0;
mSrc[3] = mFoldDrawHeight;
mSrc[4] = mFoldDrawWidth;
mSrc[5] = 0;
mSrc[6] = mFoldDrawWidth;
mSrc[7] = mFoldDrawHeight;
/*
* Computes the transformation matrix for each fold using the values
* calculated above.
*/
for (int x = 0; x < mNumberOfFolds; x++) {
boolean isEven = (x % 2 == 0);
if (mIsHorizontal) {
mDst[0] = (anchorPoint > x * mFoldDrawWidth) ? anchorPoint
+ (x - midFold) * scaledWidth : anchorPoint
- (midFold - x) * scaledWidth;
mDst[1] = isEven ? 0 : topScaledPoint;
mDst[2] = mDst[0];
mDst[3] = isEven ? mFoldDrawHeight : bottomScaledPoint;
mDst[4] = (anchorPoint > (x + 1) * mFoldDrawWidth) ? anchorPoint
+ (x + 1 - midFold) * scaledWidth
: anchorPoint - (midFold - x - 1) * scaledWidth;
mDst[5] = isEven ? topScaledPoint : 0;
mDst[6] = mDst[4];
mDst[7] = isEven ? bottomScaledPoint : mFoldDrawHeight;
} else {
mDst[0] = isEven ? 0 : leftScaledPoint;
mDst[1] = (anchorPoint > x * mFoldDrawHeight) ? anchorPoint
+ (x - midFold) * scaledHeight : anchorPoint
- (midFold - x) * scaledHeight;
mDst[2] = isEven ? leftScaledPoint : 0;
mDst[3] = (anchorPoint > (x + 1) * mFoldDrawHeight) ? anchorPoint
+ (x + 1 - midFold) * scaledHeight
: anchorPoint - (midFold - x - 1) * scaledHeight;
mDst[4] = isEven ? mFoldDrawWidth : rightScaledPoint;
mDst[5] = mDst[1];
mDst[6] = isEven ? rightScaledPoint : mFoldDrawWidth;
mDst[7] = mDst[3];
}
/*
* Pixel fractions are present for odd number of folds which need to
* be rounded off here.
*/
for (int y = 0; y < 8; y++) {
mDst[y] = Math.round(mDst[y]);
}
/*
* If it so happens that any of the folds have reached a point where
* the width or height of that fold is 0, then nothing needs to be
* drawn onto the canvas because the view is essentially completely
* folded.
*/
if (mIsHorizontal) {
if (mDst[4] <= mDst[0] || mDst[6] <= mDst[2]) {
mShouldDraw = false;
return;
}
} else {
if (mDst[3] <= mDst[1] || mDst[7] <= mDst[5]) {
mShouldDraw = false;
return;
}
}
/* Sets the shadow and bitmap transformation matrices. */
mMatrix[x].setPolyToPoly(mSrc, 0, mDst, 0, NUM_OF_POLY_POINTS / 2);
}
/*
* The shadows on the folds are split into two parts: Solid shadows and
* gradients. Every other fold has a solid shadow which overlays the
* whole fold. Similarly, the folds in between these alternating folds
* also have an overlaying shadow. However, it is a gradient that takes
* up part of the fold as opposed to a solid shadow overlaying the whole
* fold.
*/
/* Solid shadow paint object. */
int alpha = (int) (mFoldFactor * 255 * SHADING_ALPHA);
mSolidShadow.setColor(Color.argb(alpha, 0, 0, 0));
if (mIsHorizontal) {
mShadowGradientMatrix.setScale(mFoldDrawWidth, 1);
mShadowLinearGradient.setLocalMatrix(mShadowGradientMatrix);
} else {
mShadowGradientMatrix.setScale(1, mFoldDrawHeight);
mShadowLinearGradient.setLocalMatrix(mShadowGradientMatrix);
}
mGradientShadow.setAlpha(alpha);
}
@Override
protected void dispatchDraw(Canvas canvas) {
/**
* If prepareFold has not been called or if preparation has not
* completed yet, then no custom drawing will take place so only need to
* invoke super's onDraw and return.
*/
if (!mIsFoldPrepared || mFoldFactor == 0) {
super.dispatchDraw(canvas);
return;
}
if (!mShouldDraw) {
return;
}
Rect src;
/*
* Draws the bitmaps and shadows on the canvas with the appropriate
* transformations.
*/
for (int x = 0; x < mNumberOfFolds; x++) {
src = mFoldRectArray[x];
/* The canvas is saved and restored for every individual fold */
canvas.save();
/*
* Concatenates the canvas with the transformation matrix for the
* the segment of the view corresponding to the actual image being
* displayed.
*/
canvas.concat(mMatrix[x]);
if (Util.IS_JBMR2) {
mDstRect.set(0, 0, src.width(), src.height());
canvas.drawBitmap(mFullBitmap, src, mDstRect, null);
} else {
/*
* The same transformation matrix is used for both the shadow
* and the image segment. The canvas is clipped to account for
* the size of each fold and is translated so they are drawn in
* the right place. The shadow is then drawn on top of the
* different folds using the sametransformation matrix.
*/
canvas.clipRect(0, 0, src.right - src.left, src.bottom
- src.top);
if (mIsHorizontal) {
canvas.translate(-src.left, 0);
} else {
canvas.translate(0, -src.top);
}
super.dispatchDraw(canvas);
if (mIsHorizontal) {
canvas.translate(src.left, 0);
} else {
canvas.translate(0, src.top);
}
}
/* Draws the shadows corresponding to this specific fold. */
if (x % 2 == 0) {
canvas.drawRect(0, 0, mFoldDrawWidth, mFoldDrawHeight,
mSolidShadow);
} else {
canvas.drawRect(0, 0, mFoldDrawWidth, mFoldDrawHeight,
mGradientShadow);
}
canvas.restore();
}
}
}