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 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 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 }