diff --git a/samples/devbytes/graphics/FoldingLayout/AndroidManifest.xml b/samples/devbytes/graphics/FoldingLayout/AndroidManifest.xml new file mode 100644 index 000000000..13758d7cd --- /dev/null +++ b/samples/devbytes/graphics/FoldingLayout/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/samples/devbytes/graphics/FoldingLayout/res/drawable-hdpi/ic_launcher.png b/samples/devbytes/graphics/FoldingLayout/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 000000000..96a442e5b Binary files /dev/null and b/samples/devbytes/graphics/FoldingLayout/res/drawable-hdpi/ic_launcher.png differ diff --git a/samples/devbytes/graphics/FoldingLayout/res/drawable-hdpi/image.jpg b/samples/devbytes/graphics/FoldingLayout/res/drawable-hdpi/image.jpg new file mode 100644 index 000000000..60ce9f2ca Binary files /dev/null and b/samples/devbytes/graphics/FoldingLayout/res/drawable-hdpi/image.jpg differ diff --git a/samples/devbytes/graphics/FoldingLayout/res/drawable-ldpi/ic_launcher.png b/samples/devbytes/graphics/FoldingLayout/res/drawable-ldpi/ic_launcher.png new file mode 100644 index 000000000..99238729d Binary files /dev/null and b/samples/devbytes/graphics/FoldingLayout/res/drawable-ldpi/ic_launcher.png differ diff --git a/samples/devbytes/graphics/FoldingLayout/res/drawable-mdpi/ic_launcher.png b/samples/devbytes/graphics/FoldingLayout/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 000000000..359047dfa Binary files /dev/null and b/samples/devbytes/graphics/FoldingLayout/res/drawable-mdpi/ic_launcher.png differ diff --git a/samples/devbytes/graphics/FoldingLayout/res/drawable-xhdpi/ic_launcher.png b/samples/devbytes/graphics/FoldingLayout/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 000000000..71c6d760f Binary files /dev/null and b/samples/devbytes/graphics/FoldingLayout/res/drawable-xhdpi/ic_launcher.png differ diff --git a/samples/devbytes/graphics/FoldingLayout/res/layout/activity_fold.xml b/samples/devbytes/graphics/FoldingLayout/res/layout/activity_fold.xml new file mode 100644 index 000000000..9ed3bc524 --- /dev/null +++ b/samples/devbytes/graphics/FoldingLayout/res/layout/activity_fold.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/devbytes/graphics/FoldingLayout/res/layout/spinner.xml b/samples/devbytes/graphics/FoldingLayout/res/layout/spinner.xml new file mode 100644 index 000000000..c35133df1 --- /dev/null +++ b/samples/devbytes/graphics/FoldingLayout/res/layout/spinner.xml @@ -0,0 +1,18 @@ + + \ No newline at end of file diff --git a/samples/devbytes/graphics/FoldingLayout/res/menu/fold.xml b/samples/devbytes/graphics/FoldingLayout/res/menu/fold.xml new file mode 100644 index 000000000..a0231bde4 --- /dev/null +++ b/samples/devbytes/graphics/FoldingLayout/res/menu/fold.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/devbytes/graphics/FoldingLayout/res/menu/fold_with_bug.xml b/samples/devbytes/graphics/FoldingLayout/res/menu/fold_with_bug.xml new file mode 100644 index 000000000..44631ee3a --- /dev/null +++ b/samples/devbytes/graphics/FoldingLayout/res/menu/fold_with_bug.xml @@ -0,0 +1,32 @@ + + + + + + + + + + \ No newline at end of file diff --git a/samples/devbytes/graphics/FoldingLayout/res/values/strings.xml b/samples/devbytes/graphics/FoldingLayout/res/values/strings.xml new file mode 100644 index 000000000..181b15d1a --- /dev/null +++ b/samples/devbytes/graphics/FoldingLayout/res/values/strings.xml @@ -0,0 +1,42 @@ + + + + FoldingLayout + FoldActivity + + Horizontal + Vertical + Number Of Folds + + Animate + + Camera Feed + Static Image + + Sepia Off + + + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 1 + + + diff --git a/samples/devbytes/graphics/FoldingLayout/src/com/example/android/foldinglayout/FoldingLayout.java b/samples/devbytes/graphics/FoldingLayout/src/com/example/android/foldinglayout/FoldingLayout.java new file mode 100644 index 000000000..8afb27e0f --- /dev/null +++ b/samples/devbytes/graphics/FoldingLayout/src/com/example/android/foldinglayout/FoldingLayout.java @@ -0,0 +1,545 @@ +/* + * 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.example.android.foldinglayout; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Rect; +import android.graphics.Shader.TileMode; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +/** + * 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). + * + * 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. + * + * 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 FoldingLayout extends ViewGroup { + + 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; + + private Orientation mOrientation = Orientation.HORIZONTAL; + + private 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 FoldingLayout(Context context) { + super(context); + } + + public FoldingLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public FoldingLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @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 { + 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. + * + * 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 (FoldingLayoutActivity.IS_JBMR2) { + 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.onEndFold(); + } + + if (mPreviousFoldFactor == 0 && mFoldFactor > 0) { + 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 (FoldingLayoutActivity.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(); + } + } + +} \ No newline at end of file diff --git a/samples/devbytes/graphics/FoldingLayout/src/com/example/android/foldinglayout/FoldingLayoutActivity.java b/samples/devbytes/graphics/FoldingLayout/src/com/example/android/foldinglayout/FoldingLayoutActivity.java new file mode 100644 index 000000000..1a1033dcb --- /dev/null +++ b/samples/devbytes/graphics/FoldingLayout/src/com/example/android/foldinglayout/FoldingLayoutActivity.java @@ -0,0 +1,429 @@ +/* + * 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.example.android.foldinglayout; + +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.app.Activity; +import android.graphics.Color; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.Paint; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.os.Build; +import android.os.Bundle; +import android.view.GestureDetector; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.TextureView; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ImageView; +import android.widget.SeekBar; +import android.widget.Spinner; + +import com.example.android.foldinglayout.FoldingLayout.Orientation; + +import java.io.IOException; + +/** + * This application creates a paper like folding effect of some view. + * The number of folds, orientation (vertical or horizontal) of the fold, and the + * anchor point about which the view will fold can be set to achieve different + * folding effects. + * + * Using bitmap and canvas scaling techniques, the foldingLayout can be scaled so as + * to depict a paper-like folding effect. The addition of shadows on the separate folds + * adds a sense of realism to the visual effect. + * + * This application shows folding of a TextureView containing a live camera feed, + * as well as the folding of an ImageView with a static image. The TextureView experiences + * jagged edges as a result of scaling operations on rectangles. The ImageView however + * contains a 1 pixel transparent border around its contents which can be used to avoid + * this unwanted artifact. + */ +public class FoldingLayoutActivity extends Activity { + + private final int ANTIALIAS_PADDING = 1; + + private final int FOLD_ANIMATION_DURATION = 1000; + + /* 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." + */ + static final boolean IS_JBMR2 = Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR2; + + private FoldingLayout mFoldLayout; + private SeekBar mAnchorSeekBar; + private Orientation mOrientation = Orientation.HORIZONTAL; + + private int mTranslation = 0; + private int mNumberOfFolds = 2; + private int mParentPositionY = -1; + private int mTouchSlop = -1; + + private float mAnchorFactor = 0; + + private boolean mDidLoadSpinner = true; + private boolean mDidNotStartScroll = true; + + private boolean mIsCameraFeed = false; + private boolean mIsSepiaOn = true; + + private GestureDetector mScrollGestureDetector; + private ItemSelectedListener mItemSelectedListener; + + private Camera mCamera; + private TextureView mTextureView; + private ImageView mImageView; + + private Paint mSepiaPaint; + private Paint mDefaultPaint; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_fold); + + mImageView = (ImageView)findViewById(R.id.image_view); + mImageView.setPadding(ANTIALIAS_PADDING, ANTIALIAS_PADDING, ANTIALIAS_PADDING, + ANTIALIAS_PADDING); + mImageView.setScaleType(ImageView.ScaleType.FIT_XY); + mImageView.setImageDrawable(getResources().getDrawable(R.drawable.image)); + + mTextureView = new TextureView(this); + mTextureView.setSurfaceTextureListener(mSurfaceTextureListener); + + mAnchorSeekBar = (SeekBar)findViewById(R.id.anchor_seek_bar); + mFoldLayout = (FoldingLayout)findViewById(R.id.fold_view); + mFoldLayout.setBackgroundColor(Color.BLACK); + mFoldLayout.setFoldListener(mOnFoldListener); + + mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop(); + + mAnchorSeekBar.setOnSeekBarChangeListener(mSeekBarChangeListener); + + mScrollGestureDetector = new GestureDetector(this, new ScrollGestureDetector()); + mItemSelectedListener = new ItemSelectedListener(); + + mDefaultPaint = new Paint(); + mSepiaPaint = new Paint(); + + ColorMatrix m1 = new ColorMatrix(); + ColorMatrix m2 = new ColorMatrix(); + m1.setSaturation(0); + m2.setScale(1f, .95f, .82f, 1.0f); + m1.setConcat(m2, m1); + mSepiaPaint.setColorFilter(new ColorMatrixColorFilter(m1)); + } + + /** + * This listener, along with the setSepiaLayer method below, show a possible use case + * of the OnFoldListener provided with the FoldingLayout. This is a fun extra addition + * to the demo showing what kind of visual effects can be applied to the child of the + * FoldingLayout by setting the layer type to hardware. With a hardware layer type + * applied to the child, a paint object can also be applied to the same layer. Using + * the concatenation of two different color matrices (above), a color filter was created + * which simulates a sepia effect on the layer.*/ + private OnFoldListener mOnFoldListener = + new OnFoldListener() { + @Override + public void onStartFold() { + if (mIsSepiaOn) { + setSepiaLayer(mFoldLayout.getChildAt(0), true); + } + } + + @Override + public void onEndFold() { + setSepiaLayer(mFoldLayout.getChildAt(0), false); + } + }; + + private void setSepiaLayer (View view, boolean isSepiaLayerOn) { + if (!IS_JBMR2) { + if (isSepiaLayerOn) { + view.setLayerType(View.LAYER_TYPE_HARDWARE, null); + view.setLayerPaint(mSepiaPaint); + } else { + view.setLayerPaint(mDefaultPaint); + } + } + } + + /** + * Creates a SurfaceTextureListener in order to prepare a TextureView + * which displays a live, and continuously updated, feed from the Camera. + */ + private TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView + .SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i2) { + mCamera = Camera.open(); + + if (mCamera == null && Camera.getNumberOfCameras() > 1) { + mCamera = mCamera.open(Camera.CameraInfo.CAMERA_FACING_FRONT); + } + + if (mCamera == null) { + return; + } + + try { + mCamera.setPreviewTexture(surfaceTexture); + mCamera.setDisplayOrientation(90); + mCamera.startPreview(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i2) { + // Ignored, Camera does all the work for us + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { + if (mCamera != null) { + mCamera.stopPreview(); + mCamera.release(); + } + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { + // Invoked every time there's a new Camera preview frame + } + }; + + /** + * A listener for scrolling changes in the seekbar. The anchor point of the folding + * view is updated every time the seekbar stops tracking touch events. Every time the + * anchor point is updated, the folding view is restored to a default unfolded state. + */ + private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener = new SeekBar + .OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int i, boolean b) { + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + mTranslation = 0; + mAnchorFactor = ((float)mAnchorSeekBar.getProgress())/100.0f; + mFoldLayout.setAnchorFactor(mAnchorFactor); + } + }; + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (IS_JBMR2) { + getMenuInflater().inflate(R.menu.fold_with_bug, menu); + } else { + getMenuInflater().inflate(R.menu.fold, menu); + } + Spinner s = (Spinner) menu.findItem(R.id.num_of_folds).getActionView(); + s.setOnItemSelectedListener(mItemSelectedListener); + return true; + } + + @Override + public void onWindowFocusChanged (boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + + int[] loc = new int[2]; + mFoldLayout.getLocationOnScreen(loc); + mParentPositionY = loc[1]; + } + + @Override + public boolean onTouchEvent(MotionEvent me) { + return mScrollGestureDetector.onTouchEvent(me); + } + + @Override + public boolean onOptionsItemSelected (MenuItem item) { + switch(item.getItemId()) { + case R.id.animate_fold: + animateFold(); + break; + case R.id.toggle_orientation: + mOrientation = (mOrientation == Orientation.HORIZONTAL) ? Orientation.VERTICAL : + Orientation.HORIZONTAL; + item.setTitle((mOrientation == Orientation.HORIZONTAL) ? R.string.vertical : + R.string.horizontal); + mTranslation = 0; + mFoldLayout.setOrientation(mOrientation); + break; + case R.id.camera_feed: + mIsCameraFeed = !mIsCameraFeed; + item.setTitle(mIsCameraFeed ? R.string.static_image : R.string.camera_feed); + item.setChecked(mIsCameraFeed); + if (mIsCameraFeed) { + mFoldLayout.removeView(mImageView); + mFoldLayout.addView(mTextureView, new ViewGroup.LayoutParams( + mFoldLayout.getWidth(), mFoldLayout.getHeight())); + } else { + mFoldLayout.removeView(mTextureView); + mFoldLayout.addView(mImageView, new ViewGroup.LayoutParams( + mFoldLayout.getWidth(), mFoldLayout.getHeight())); + } + mTranslation = 0; + break; + case R.id.sepia: + mIsSepiaOn = !mIsSepiaOn; + item.setChecked(!mIsSepiaOn); + if (mIsSepiaOn && mFoldLayout.getFoldFactor() != 0) { + setSepiaLayer(mFoldLayout.getChildAt(0), true); + } else { + setSepiaLayer(mFoldLayout.getChildAt(0), false); + } + break; + default: + break; + + } + return super.onOptionsItemSelected(item); + } + + /** + * Animates the folding view inwards (to a completely folded state) from its + * current state and then back out to its original state. + */ + public void animateFold () + { + float foldFactor = mFoldLayout.getFoldFactor(); + + ObjectAnimator animator = ObjectAnimator.ofFloat(mFoldLayout, "foldFactor", foldFactor, 1); + animator.setRepeatMode(ValueAnimator.REVERSE); + animator.setRepeatCount(1); + animator.setDuration(FOLD_ANIMATION_DURATION); + animator.setInterpolator(new AccelerateInterpolator()); + animator.start(); + } + + /** + * Listens for selection events of the spinner located on the action bar. Every + * time a new value is selected, the number of folds in the folding view is updated + * and is also restored to a default unfolded state. + */ + private class ItemSelectedListener implements OnItemSelectedListener { + @Override + public void onItemSelected(AdapterView parent, View view, int pos, long id) { + mNumberOfFolds = Integer.parseInt(parent.getItemAtPosition(pos).toString()); + if (mDidLoadSpinner) { + mDidLoadSpinner = false; + } else { + mTranslation = 0; + mFoldLayout.setNumberOfFolds(mNumberOfFolds); + } + } + + @Override + public void onNothingSelected(AdapterView arg0) { + } + } + + /** This class uses user touch events to fold and unfold the folding view. */ + private class ScrollGestureDetector extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onDown (MotionEvent e) { + mDidNotStartScroll = true; + return true; + } + + /** + * All the logic here is used to determine by what factor the paper view should + * be folded in response to the user's touch events. The logic here uses vertical + * scrolling to fold a vertically oriented view and horizontal scrolling to fold + * a horizontally oriented fold. Depending on where the anchor point of the fold is, + * movements towards or away from the anchor point will either fold or unfold + * the paper respectively. + * + * The translation logic here also accounts for the touch slop when a new user touch + * begins, but before a scroll event is first invoked. + */ + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + int touchSlop = 0; + float factor; + if (mOrientation == Orientation.VERTICAL) { + factor = Math.abs((float)(mTranslation) / (float)(mFoldLayout.getHeight())); + + if (e2.getY() - mParentPositionY <= mFoldLayout.getHeight() + && e2.getY() - mParentPositionY >= 0) { + if ((e2.getY() - mParentPositionY) > mFoldLayout.getHeight() * mAnchorFactor) { + mTranslation -= (int)distanceY; + touchSlop = distanceY < 0 ? -mTouchSlop : mTouchSlop; + } else { + mTranslation += (int)distanceY; + touchSlop = distanceY < 0 ? mTouchSlop : -mTouchSlop; + } + mTranslation = mDidNotStartScroll ? mTranslation + touchSlop : mTranslation; + + if (mTranslation < -mFoldLayout.getHeight()) { + mTranslation = -mFoldLayout.getHeight(); + } + } + } else { + factor = Math.abs(((float)mTranslation) / ((float) mFoldLayout.getWidth())); + + if (e2.getRawX() > mFoldLayout.getWidth() * mAnchorFactor) { + mTranslation -= (int)distanceX; + touchSlop = distanceX < 0 ? -mTouchSlop : mTouchSlop; + } else { + mTranslation += (int)distanceX; + touchSlop = distanceX < 0 ? mTouchSlop : -mTouchSlop; + } + mTranslation = mDidNotStartScroll ? mTranslation + touchSlop : mTranslation; + + if (mTranslation < -mFoldLayout.getWidth()) { + mTranslation = -mFoldLayout.getWidth(); + } + } + + mDidNotStartScroll = false; + + if (mTranslation > 0) { + mTranslation = 0; + } + + mFoldLayout.setFoldFactor(factor); + + return true; + } + } +} \ No newline at end of file diff --git a/samples/devbytes/graphics/FoldingLayout/src/com/example/android/foldinglayout/OnFoldListener.java b/samples/devbytes/graphics/FoldingLayout/src/com/example/android/foldinglayout/OnFoldListener.java new file mode 100644 index 000000000..a305568fc --- /dev/null +++ b/samples/devbytes/graphics/FoldingLayout/src/com/example/android/foldinglayout/OnFoldListener.java @@ -0,0 +1,27 @@ +/* + * 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.example.android.foldinglayout; + +/** + * This interface listens for when the folding layout begins folding (enters + * a folded state from a completely unfolded state), or ends folding (enters a + * completely unfolded state from a folded state). + */ +public interface OnFoldListener { + public void onStartFold(); + public void onEndFold(); +}