1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.android.foldinglayout;
18 
19 import android.content.Context;
20 import android.graphics.Bitmap;
21 import android.graphics.Canvas;
22 import android.graphics.Color;
23 import android.graphics.LinearGradient;
24 import android.graphics.Matrix;
25 import android.graphics.Paint;
26 import android.graphics.Paint.Style;
27 import android.graphics.Rect;
28 import android.graphics.Shader.TileMode;
29 import android.util.AttributeSet;
30 import android.view.View;
31 import android.view.ViewGroup;
32 
33 /**
34  * The folding layout where the number of folds, the anchor point and the
35  * orientation of the fold can be specified. Each of these parameters can
36  * be modified individually and updates and resets the fold to a default
37  * (unfolded) state. The fold factor varies between 0 (completely unfolded
38  * flat image) to 1.0 (completely folded, non-visible image).
39  *
40  * This layout throws an exception if there is more than one child added to the view.
41  * For more complicated view hierarchy's inside the folding layout, the views should all
42  * be nested inside 1 parent layout.
43  *
44  * This layout folds the contents of its child in real time. By applying matrix
45  * transformations when drawing to canvas, the contents of the child may change as
46  * the fold takes place. It is important to note that there are jagged edges about
47  * the perimeter of the layout as a result of applying transformations to a rectangle.
48  * This can be avoided by having the child of this layout wrap its content inside a
49  * 1 pixel transparent border. This will cause an anti-aliasing like effect and smoothen
50  * out the edges.
51  *
52  */
53 public class FoldingLayout extends ViewGroup {
54 
55     public static enum Orientation {
56         VERTICAL,
57         HORIZONTAL
58     }
59 
60     private final String FOLDING_VIEW_EXCEPTION_MESSAGE = "Folding Layout can only 1 child at " +
61             "most";
62 
63     private final float SHADING_ALPHA = 0.8f;
64     private final float SHADING_FACTOR = 0.5f;
65     private final int DEPTH_CONSTANT = 1500;
66     private final int NUM_OF_POLY_POINTS = 8;
67 
68     private Rect[] mFoldRectArray;
69 
70     private Matrix [] mMatrix;
71 
72     private Orientation mOrientation = Orientation.HORIZONTAL;
73 
74     private float mAnchorFactor = 0;
75     private float mFoldFactor = 0;
76 
77     private int mNumberOfFolds = 2;
78 
79     private boolean mIsHorizontal = true;
80 
81     private int mOriginalWidth = 0;
82     private int mOriginalHeight = 0;
83 
84     private float mFoldMaxWidth = 0;
85     private float mFoldMaxHeight = 0;
86     private float mFoldDrawWidth = 0;
87     private float mFoldDrawHeight = 0;
88 
89     private boolean mIsFoldPrepared = false;
90     private boolean mShouldDraw = true;
91 
92     private Paint mSolidShadow;
93     private Paint mGradientShadow;
94     private LinearGradient mShadowLinearGradient;
95     private Matrix mShadowGradientMatrix;
96 
97     private float [] mSrc;
98     private float [] mDst;
99 
100     private OnFoldListener mFoldListener;
101 
102     private float mPreviousFoldFactor = 0;
103 
104     private Bitmap mFullBitmap;
105     private Rect mDstRect;
106 
FoldingLayout(Context context)107     public FoldingLayout(Context context) {
108         super(context);
109     }
110 
FoldingLayout(Context context, AttributeSet attrs)111     public FoldingLayout(Context context, AttributeSet attrs) {
112         super(context, attrs);
113     }
114 
FoldingLayout(Context context, AttributeSet attrs, int defStyle)115     public FoldingLayout(Context context, AttributeSet attrs, int defStyle) {
116         super(context, attrs, defStyle);
117     }
118 
119     @Override
addViewInLayout(View child, int index, LayoutParams params, boolean preventRequestLayout)120     protected boolean addViewInLayout(View child, int index, LayoutParams params,
121                                       boolean preventRequestLayout) {
122         throwCustomException(getChildCount());
123         boolean returnValue = super.addViewInLayout(child, index, params, preventRequestLayout);
124         return returnValue;
125     }
126 
127     @Override
addView(View child, int index, LayoutParams params)128     public void addView(View child, int index, LayoutParams params) {
129         throwCustomException(getChildCount());
130         super.addView(child, index, params);
131     }
132 
133     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)134     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
135         View child = getChildAt(0);
136         measureChild(child,widthMeasureSpec, heightMeasureSpec);
137         setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
138     }
139 
140     @Override
onLayout(boolean changed, int l, int t, int r, int b)141     protected void onLayout(boolean changed, int l, int t, int r, int b) {
142         View child = getChildAt(0);
143         child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
144         updateFold();
145     }
146 
147     /**
148      * The custom exception to be thrown so as to limit the number of views in this
149      * layout to at most one.
150      */
151     private class NumberOfFoldingLayoutChildrenException extends RuntimeException {
NumberOfFoldingLayoutChildrenException(String message)152         public NumberOfFoldingLayoutChildrenException(String message) {
153             super(message);
154         }
155     }
156 
157     /** Throws an exception if the number of views added to this layout exceeds one.*/
throwCustomException(int numOfChildViews)158     private void throwCustomException (int numOfChildViews) {
159         if (numOfChildViews == 1) {
160             throw new NumberOfFoldingLayoutChildrenException(FOLDING_VIEW_EXCEPTION_MESSAGE);
161         }
162     }
163 
setFoldListener(OnFoldListener foldListener)164     public void setFoldListener(OnFoldListener foldListener) {
165         mFoldListener = foldListener;
166     }
167 
168     /**
169      * Sets the fold factor of the folding view and updates all the corresponding
170      * matrices and values to account for the new fold factor. Once that is complete,
171      * it redraws itself with the new fold. */
setFoldFactor(float foldFactor)172     public void setFoldFactor(float foldFactor) {
173         if (foldFactor != mFoldFactor) {
174             mFoldFactor = foldFactor;
175             calculateMatrices();
176             invalidate();
177         }
178     }
179 
setOrientation(Orientation orientation)180     public void setOrientation(Orientation orientation) {
181         if (orientation != mOrientation) {
182             mOrientation = orientation;
183             updateFold();
184         }
185     }
186 
setAnchorFactor(float anchorFactor)187     public void setAnchorFactor(float anchorFactor) {
188         if (anchorFactor != mAnchorFactor) {
189             mAnchorFactor = anchorFactor;
190             updateFold();
191         }
192     }
193 
setNumberOfFolds(int numberOfFolds)194     public void setNumberOfFolds(int numberOfFolds) {
195         if (numberOfFolds != mNumberOfFolds) {
196             mNumberOfFolds = numberOfFolds;
197             updateFold();
198         }
199     }
200 
getAnchorFactor()201     public float getAnchorFactor() {
202         return mAnchorFactor;
203     }
204 
getOrientation()205     public Orientation getOrientation() {
206         return mOrientation;
207     }
208 
getFoldFactor()209     public float getFoldFactor() {
210         return mFoldFactor;
211     }
212 
getNumberOfFolds()213     public int getNumberOfFolds() {
214         return mNumberOfFolds;
215     }
216 
updateFold()217     private void updateFold() {
218         prepareFold(mOrientation, mAnchorFactor, mNumberOfFolds);
219         calculateMatrices();
220         invalidate();
221     }
222 
223     /**
224      * This method is called in order to update the fold's orientation, anchor
225      * point and number of folds. This creates the necessary setup in order to
226      * prepare the layout for a fold with the specified parameters. Some of the
227      * dimensions required for the folding transformation are also acquired here.
228      *
229      * After this method is called, it will be in a completely unfolded state by default.
230      */
prepareFold(Orientation orientation, float anchorFactor, int numberOfFolds)231     private void prepareFold(Orientation orientation, float anchorFactor, int numberOfFolds) {
232 
233         mSrc = new float[NUM_OF_POLY_POINTS];
234         mDst = new float[NUM_OF_POLY_POINTS];
235 
236         mDstRect = new Rect();
237 
238         mFoldFactor = 0;
239         mPreviousFoldFactor = 0;
240 
241         mIsFoldPrepared = false;
242 
243         mSolidShadow = new Paint();
244         mGradientShadow = new Paint();
245 
246         mOrientation = orientation;
247         mIsHorizontal = (orientation == Orientation.HORIZONTAL);
248 
249         if (mIsHorizontal) {
250             mShadowLinearGradient = new LinearGradient(0, 0, SHADING_FACTOR, 0, Color.BLACK,
251                     Color.TRANSPARENT, TileMode.CLAMP);
252         } else {
253             mShadowLinearGradient = new LinearGradient(0, 0, 0, SHADING_FACTOR, Color.BLACK,
254                     Color.TRANSPARENT, TileMode.CLAMP);
255         }
256 
257         mGradientShadow.setStyle(Style.FILL);
258         mGradientShadow.setShader(mShadowLinearGradient);
259         mShadowGradientMatrix = new Matrix();
260 
261         mAnchorFactor = anchorFactor;
262         mNumberOfFolds = numberOfFolds;
263 
264         mOriginalWidth = getMeasuredWidth();
265         mOriginalHeight = getMeasuredHeight();
266 
267         mFoldRectArray = new Rect[mNumberOfFolds];
268         mMatrix = new Matrix [mNumberOfFolds];
269 
270         for (int x = 0; x < mNumberOfFolds; x++) {
271             mMatrix[x] = new Matrix();
272         }
273 
274         int h = mOriginalHeight;
275         int w = mOriginalWidth;
276 
277         if (FoldingLayoutActivity.IS_JBMR2) {
278             mFullBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
279             Canvas canvas = new Canvas(mFullBitmap);
280             getChildAt(0).draw(canvas);
281         }
282 
283         int delta = Math.round(mIsHorizontal ? ((float) w) / ((float) mNumberOfFolds) :
284                 ((float) h) /((float) mNumberOfFolds));
285 
286         /* Loops through the number of folds and segments the full layout into a number
287          * of smaller equal components. If the number of folds is odd, then one of the
288          * components will be smaller than all the rest. Note that deltap below handles
289          * the calculation for an odd number of folds.*/
290         for (int x = 0; x < mNumberOfFolds; x++) {
291             if (mIsHorizontal) {
292                 int deltap = (x + 1) * delta > w ? w - x * delta : delta;
293                 mFoldRectArray[x] = new Rect(x * delta, 0, x * delta + deltap, h);
294             } else {
295                 int deltap = (x + 1) * delta > h ? h - x * delta : delta;
296                 mFoldRectArray[x] = new Rect(0, x * delta, w, x * delta + deltap);
297             }
298         }
299 
300         if (mIsHorizontal) {
301             mFoldMaxHeight = h;
302             mFoldMaxWidth = delta;
303         } else {
304             mFoldMaxHeight = delta;
305             mFoldMaxWidth = w;
306         }
307 
308         mIsFoldPrepared = true;
309     }
310 
311     /*
312     * Calculates the transformation matrices used to draw each of the separate folding
313     * segments from this view.
314     */
calculateMatrices()315     private void calculateMatrices() {
316 
317         mShouldDraw = true;
318 
319         if (!mIsFoldPrepared) {
320             return;
321         }
322 
323         /** If the fold factor is 1 than the folding view should not be seen
324          * and the canvas can be left completely empty. */
325         if (mFoldFactor == 1) {
326             mShouldDraw = false;
327             return;
328         }
329 
330         if (mFoldFactor == 0 &&  mPreviousFoldFactor > 0) {
331             mFoldListener.onEndFold();
332         }
333 
334         if (mPreviousFoldFactor == 0 && mFoldFactor > 0) {
335             mFoldListener.onStartFold();
336         }
337 
338         mPreviousFoldFactor = mFoldFactor;
339 
340         /* Reset all the transformation matrices back to identity before computing
341          * the new transformation */
342         for (int x = 0; x < mNumberOfFolds; x++) {
343             mMatrix[x].reset();
344         }
345 
346         float cTranslationFactor = 1 - mFoldFactor;
347 
348         float translatedDistance = mIsHorizontal ? mOriginalWidth * cTranslationFactor :
349                 mOriginalHeight * cTranslationFactor;
350 
351         float translatedDistancePerFold = Math.round(translatedDistance / mNumberOfFolds);
352 
353         /* For an odd number of folds, the rounding error may cause the
354          * translatedDistancePerFold to be grater than the max fold width or height. */
355         mFoldDrawWidth = mFoldMaxWidth < translatedDistancePerFold ?
356                 translatedDistancePerFold : mFoldMaxWidth;
357         mFoldDrawHeight = mFoldMaxHeight < translatedDistancePerFold ?
358                 translatedDistancePerFold : mFoldMaxHeight;
359 
360         float translatedDistanceFoldSquared = translatedDistancePerFold * translatedDistancePerFold;
361 
362         /* Calculate the depth of the fold into the screen using pythagorean theorem. */
363         float depth = mIsHorizontal ?
364                 (float)Math.sqrt((double)(mFoldDrawWidth * mFoldDrawWidth -
365                         translatedDistanceFoldSquared)) :
366                 (float)Math.sqrt((double)(mFoldDrawHeight * mFoldDrawHeight -
367                         translatedDistanceFoldSquared));
368 
369         /* The size of some object is always inversely proportional to the distance
370         *  it is away from the viewpoint. The constant can be varied to to affect the
371         *  amount of perspective. */
372         float scaleFactor = DEPTH_CONSTANT / (DEPTH_CONSTANT + depth);
373 
374         float scaledWidth, scaledHeight, bottomScaledPoint, topScaledPoint, rightScaledPoint,
375                 leftScaledPoint;
376 
377         if (mIsHorizontal) {
378             scaledWidth = mFoldDrawWidth * cTranslationFactor;
379             scaledHeight = mFoldDrawHeight * scaleFactor;
380         } else {
381             scaledWidth = mFoldDrawWidth * scaleFactor;
382             scaledHeight = mFoldDrawHeight * cTranslationFactor;
383         }
384 
385         topScaledPoint = (mFoldDrawHeight - scaledHeight) / 2.0f;
386         bottomScaledPoint = topScaledPoint + scaledHeight;
387 
388         leftScaledPoint = (mFoldDrawWidth - scaledWidth) / 2.0f;
389         rightScaledPoint = leftScaledPoint + scaledWidth;
390 
391         float anchorPoint = mIsHorizontal ? mAnchorFactor * mOriginalWidth :
392                 mAnchorFactor * mOriginalHeight;
393 
394         /* The fold along which the anchor point is located. */
395         float midFold = mIsHorizontal ? (anchorPoint / mFoldDrawWidth) : anchorPoint /
396                 mFoldDrawHeight;
397 
398         mSrc[0] = 0;
399         mSrc[1] = 0;
400         mSrc[2] = 0;
401         mSrc[3] = mFoldDrawHeight;
402         mSrc[4] = mFoldDrawWidth;
403         mSrc[5] = 0;
404         mSrc[6] = mFoldDrawWidth;
405         mSrc[7] = mFoldDrawHeight;
406 
407         /* Computes the transformation matrix for each fold using the values calculated above. */
408         for (int x = 0; x < mNumberOfFolds; x++) {
409 
410             boolean isEven = (x % 2 == 0);
411 
412             if (mIsHorizontal) {
413                 mDst[0] = (anchorPoint > x * mFoldDrawWidth) ? anchorPoint + (x - midFold) *
414                         scaledWidth : anchorPoint - (midFold - x) * scaledWidth;
415                 mDst[1] = isEven ? 0 : topScaledPoint;
416                 mDst[2] = mDst[0];
417                 mDst[3] = isEven ? mFoldDrawHeight: bottomScaledPoint;
418                 mDst[4] = (anchorPoint > (x + 1) * mFoldDrawWidth) ? anchorPoint + (x + 1 - midFold)
419                         * scaledWidth : anchorPoint - (midFold - x - 1) * scaledWidth;
420                 mDst[5] = isEven ? topScaledPoint : 0;
421                 mDst[6] = mDst[4];
422                 mDst[7] = isEven ? bottomScaledPoint : mFoldDrawHeight;
423 
424             } else {
425                 mDst[0] = isEven ? 0 : leftScaledPoint;
426                 mDst[1] = (anchorPoint > x * mFoldDrawHeight) ? anchorPoint + (x - midFold) *
427                         scaledHeight : anchorPoint - (midFold - x) * scaledHeight;
428                 mDst[2] = isEven ? leftScaledPoint: 0;
429                 mDst[3] = (anchorPoint > (x + 1) * mFoldDrawHeight) ? anchorPoint + (x + 1 -
430                         midFold) * scaledHeight : anchorPoint - (midFold - x - 1) * scaledHeight;
431                 mDst[4] = isEven ? mFoldDrawWidth : rightScaledPoint;
432                 mDst[5] = mDst[1];
433                 mDst[6] = isEven ? rightScaledPoint : mFoldDrawWidth;
434                 mDst[7] = mDst[3];
435             }
436 
437             /* Pixel fractions are present for odd number of folds which need to be
438              * rounded off here.*/
439             for (int y = 0; y < 8; y ++) {
440                 mDst[y] = Math.round(mDst[y]);
441             }
442 
443             /* If it so happens that any of the folds have reached a point where
444             *  the width or height of that fold is 0, then nothing needs to be
445             *  drawn onto the canvas because the view is essentially completely
446             *  folded.*/
447             if (mIsHorizontal) {
448                 if (mDst[4] <= mDst[0] || mDst[6] <= mDst[2]) {
449                     mShouldDraw = false;
450                     return;
451                 }
452             } else {
453                 if (mDst[3] <= mDst[1] || mDst[7] <= mDst[5]) {
454                     mShouldDraw = false;
455                     return;
456                 }
457             }
458 
459             /* Sets the shadow and bitmap transformation matrices.*/
460             mMatrix[x].setPolyToPoly(mSrc, 0, mDst, 0, NUM_OF_POLY_POINTS / 2);
461         }
462         /* The shadows on the folds are split into two parts: Solid shadows and gradients.
463          * Every other fold has a solid shadow which overlays the whole fold. Similarly,
464          * the folds in between these alternating folds also have an overlaying shadow.
465          * However, it is a gradient that takes up part of the fold as opposed to a solid
466          * shadow overlaying the whole fold.*/
467 
468         /* Solid shadow paint object. */
469         int alpha = (int) (mFoldFactor * 255 * SHADING_ALPHA);
470 
471         mSolidShadow.setColor(Color.argb(alpha, 0, 0, 0));
472 
473         if (mIsHorizontal) {
474             mShadowGradientMatrix.setScale(mFoldDrawWidth, 1);
475             mShadowLinearGradient.setLocalMatrix(mShadowGradientMatrix);
476         } else {
477             mShadowGradientMatrix.setScale(1, mFoldDrawHeight);
478             mShadowLinearGradient.setLocalMatrix(mShadowGradientMatrix);
479         }
480         mGradientShadow.setShader(mShadowLinearGradient);
481 
482         mGradientShadow.setAlpha(alpha);
483     }
484 
485     @Override
486     protected void dispatchDraw(Canvas canvas) {
487         /** If prepareFold has not been called or if preparation has not completed yet,
488          * then no custom drawing will take place so only need to invoke super's
489          * onDraw and return. */
490         if (!mIsFoldPrepared || mFoldFactor == 0) {
491             super.dispatchDraw(canvas);
492             return;
493         }
494 
495         if (!mShouldDraw) {
496             return;
497         }
498 
499         Rect src;
500          /* Draws the bitmaps and shadows on the canvas with the appropriate transformations. */
501         for (int x = 0; x < mNumberOfFolds; x++) {
502 
503             src = mFoldRectArray[x];
504             /* The canvas is saved and restored for every individual fold*/
505             canvas.save();
506 
507             /* Concatenates the canvas with the transformation matrix for the
508              *  the segment of the view corresponding to the actual image being
509              *  displayed. */
510             canvas.concat(mMatrix[x]);
511             if (FoldingLayoutActivity.IS_JBMR2) {
512                 mDstRect.set(0, 0, src.width(), src.height());
513                 canvas.drawBitmap(mFullBitmap, src, mDstRect, null);
514             } else {
515                 /* The same transformation matrix is used for both the shadow and the image
516                  * segment. The canvas is clipped to account for the size of each fold and
517                  * is translated so they are drawn in the right place. The shadow is then drawn on
518                  * top of the different folds using the sametransformation matrix.*/
519                 canvas.clipRect(0, 0, src.right - src.left, src.bottom - src.top);
520 
521                 if (mIsHorizontal) {
522                     canvas.translate(-src.left, 0);
523                 } else {
524                     canvas.translate(0, -src.top);
525                 }
526 
527                 super.dispatchDraw(canvas);
528 
529                 if (mIsHorizontal) {
530                     canvas.translate(src.left, 0);
531                 } else {
532                     canvas.translate(0, src.top);
533                 }
534             }
535             /* Draws the shadows corresponding to this specific fold. */
536             if (x % 2 == 0) {
537                 canvas.drawRect(0, 0, mFoldDrawWidth, mFoldDrawHeight, mSolidShadow);
538             } else {
539                 canvas.drawRect(0, 0, mFoldDrawWidth, mFoldDrawHeight, mGradientShadow);
540             }
541 
542             canvas.restore();
543         }
544     }
545 
546 }