blob: 8afb27e0fc94b5b7d42c4ba1b4051925184fdb4b [file] [log] [blame]
Daniel Olshansky371fcc22013-07-02 15:12:05 -07001/*
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
17package com.example.android.foldinglayout;
18
19import android.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.Canvas;
22import android.graphics.Color;
23import android.graphics.LinearGradient;
24import android.graphics.Matrix;
25import android.graphics.Paint;
26import android.graphics.Paint.Style;
27import android.graphics.Rect;
28import android.graphics.Shader.TileMode;
29import android.util.AttributeSet;
30import android.view.View;
31import 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 */
53public 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
107 public FoldingLayout(Context context) {
108 super(context);
109 }
110
111 public FoldingLayout(Context context, AttributeSet attrs) {
112 super(context, attrs);
113 }
114
115 public FoldingLayout(Context context, AttributeSet attrs, int defStyle) {
116 super(context, attrs, defStyle);
117 }
118
119 @Override
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
128 public void addView(View child, int index, LayoutParams params) {
129 throwCustomException(getChildCount());
130 super.addView(child, index, params);
131 }
132
133 @Override
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
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 {
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.*/
158 private void throwCustomException (int numOfChildViews) {
159 if (numOfChildViews == 1) {
160 throw new NumberOfFoldingLayoutChildrenException(FOLDING_VIEW_EXCEPTION_MESSAGE);
161 }
162 }
163
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. */
172 public void setFoldFactor(float foldFactor) {
173 if (foldFactor != mFoldFactor) {
174 mFoldFactor = foldFactor;
175 calculateMatrices();
176 invalidate();
177 }
178 }
179
180 public void setOrientation(Orientation orientation) {
181 if (orientation != mOrientation) {
182 mOrientation = orientation;
183 updateFold();
184 }
185 }
186
187 public void setAnchorFactor(float anchorFactor) {
188 if (anchorFactor != mAnchorFactor) {
189 mAnchorFactor = anchorFactor;
190 updateFold();
191 }
192 }
193
194 public void setNumberOfFolds(int numberOfFolds) {
195 if (numberOfFolds != mNumberOfFolds) {
196 mNumberOfFolds = numberOfFolds;
197 updateFold();
198 }
199 }
200
201 public float getAnchorFactor() {
202 return mAnchorFactor;
203 }
204
205 public Orientation getOrientation() {
206 return mOrientation;
207 }
208
209 public float getFoldFactor() {
210 return mFoldFactor;
211 }
212
213 public int getNumberOfFolds() {
214 return mNumberOfFolds;
215 }
216
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 */
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 */
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
481 mGradientShadow.setAlpha(alpha);
482 }
483
484 @Override
485 protected void dispatchDraw(Canvas canvas) {
486 /** If prepareFold has not been called or if preparation has not completed yet,
487 * then no custom drawing will take place so only need to invoke super's
488 * onDraw and return. */
489 if (!mIsFoldPrepared || mFoldFactor == 0) {
490 super.dispatchDraw(canvas);
491 return;
492 }
493
494 if (!mShouldDraw) {
495 return;
496 }
497
498 Rect src;
499 /* Draws the bitmaps and shadows on the canvas with the appropriate transformations. */
500 for (int x = 0; x < mNumberOfFolds; x++) {
501
502 src = mFoldRectArray[x];
503 /* The canvas is saved and restored for every individual fold*/
504 canvas.save();
505
506 /* Concatenates the canvas with the transformation matrix for the
507 * the segment of the view corresponding to the actual image being
508 * displayed. */
509 canvas.concat(mMatrix[x]);
510 if (FoldingLayoutActivity.IS_JBMR2) {
511 mDstRect.set(0, 0, src.width(), src.height());
512 canvas.drawBitmap(mFullBitmap, src, mDstRect, null);
513 } else {
514 /* The same transformation matrix is used for both the shadow and the image
515 * segment. The canvas is clipped to account for the size of each fold and
516 * is translated so they are drawn in the right place. The shadow is then drawn on
517 * top of the different folds using the sametransformation matrix.*/
518 canvas.clipRect(0, 0, src.right - src.left, src.bottom - src.top);
519
520 if (mIsHorizontal) {
521 canvas.translate(-src.left, 0);
522 } else {
523 canvas.translate(0, -src.top);
524 }
525
526 super.dispatchDraw(canvas);
527
528 if (mIsHorizontal) {
529 canvas.translate(src.left, 0);
530 } else {
531 canvas.translate(0, src.top);
532 }
533 }
534 /* Draws the shadows corresponding to this specific fold. */
535 if (x % 2 == 0) {
536 canvas.drawRect(0, 0, mFoldDrawWidth, mFoldDrawHeight, mSolidShadow);
537 } else {
538 canvas.drawRect(0, 0, mFoldDrawWidth, mFoldDrawHeight, mGradientShadow);
539 }
540
541 canvas.restore();
542 }
543 }
544
545}