Home | History | Annotate | Download | only in foldinglayout
      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 }