First commit for FoldingLayout.
Change-Id: I3eaca79e3c7a0c6d2e8f5c6de5a3d4c3db4bf5a5
(cherry picked from commit 425e318026b116d94b4dcae1f1a72342bf396486)
diff --git a/samples/devbytes/graphics/FoldingLayout/AndroidManifest.xml b/samples/devbytes/graphics/FoldingLayout/AndroidManifest.xml
new file mode 100644
index 0000000..13758d7
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android.foldinglayout"
+ android:versionCode="1"
+ android:versionName="1.0">
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-feature android:name="android.hardware.camera" />
+ <uses-sdk android:minSdkVersion="17"
+ android:targetSdkVersion="18"/>
+ <application android:label="@string/app_name" android:icon="@drawable/ic_launcher">
+ <activity android:name=".FoldingLayoutActivity"
+ android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
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 0000000..96a442e
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/drawable-hdpi/ic_launcher.png
Binary files 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 0000000..60ce9f2
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/drawable-hdpi/image.jpg
Binary files 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 0000000..9923872
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/drawable-ldpi/ic_launcher.png
Binary files 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 0000000..359047d
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/drawable-mdpi/ic_launcher.png
Binary files 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 0000000..71c6d76
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/drawable-xhdpi/ic_launcher.png
Binary files 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 0000000..9ed3bc5
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/layout/activity_fold.xml
@@ -0,0 +1,41 @@
+<!-- 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <com.example.android.foldinglayout.FoldingLayout
+ android:layout_weight="1"
+ android:id="@+id/fold_view"
+ android:layout_width="match_parent"
+ android:layout_height="0dp">
+
+ <ImageView
+ android:id="@+id/image_view"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:scaleType="fitXY"/>
+
+ </com.example.android.foldinglayout.FoldingLayout>
+
+ <SeekBar
+ android:id="@+id/anchor_seek_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:max="100"/>
+
+</LinearLayout>
\ 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 0000000..c35133d
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/layout/spinner.xml
@@ -0,0 +1,18 @@
+<!-- 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.
+-->
+<Spinner xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:entries="@array/num_of_folds_array" />
\ 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 0000000..a0231bd
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/menu/fold.xml
@@ -0,0 +1,41 @@
+<!-- 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/toggle_orientation"
+ android:showAsAction="never"
+ android:title="@string/vertical"/>
+
+ <item
+ android:id="@+id/num_of_folds"
+ android:showAsAction="ifRoom"
+ android:actionLayout="@layout/spinner"/>
+
+ <item
+ android:title="@string/sepia_effect_off"
+ android:id="@+id/sepia"
+ android:checkable="true"/>
+
+ <item
+ android:title="@string/camera_feed"
+ android:id="@+id/camera_feed"/>
+
+ <item
+ android:id="@+id/animate_fold"
+ android:showAsAction="never"
+ android:title="@string/animate"/>
+
+</menu>
\ 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 0000000..44631ee
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/menu/fold_with_bug.xml
@@ -0,0 +1,32 @@
+<!-- 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/toggle_orientation"
+ android:showAsAction="never"
+ android:title="@string/vertical"/>
+
+ <item
+ android:id="@+id/num_of_folds"
+ android:showAsAction="ifRoom"
+ android:actionLayout="@layout/spinner"/>
+
+ <item
+ android:id="@+id/animate_fold"
+ android:showAsAction="never"
+ android:title="@string/animate"/>
+
+</menu>
\ 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 0000000..181b15d
--- /dev/null
+++ b/samples/devbytes/graphics/FoldingLayout/res/values/strings.xml
@@ -0,0 +1,42 @@
+<!-- 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.
+-->
+<resources>
+
+ <string name="app_name">FoldingLayout</string>
+ <string name="title_activity_fold">FoldActivity</string>
+
+ <string name="horizontal">Horizontal</string>
+ <string name="vertical">Vertical</string>
+ <string name="num_of_folds">Number Of Folds</string>
+
+ <string name="animate">Animate</string>
+
+ <string name="camera_feed">Camera Feed</string>
+ <string name="static_image">Static Image</string>
+
+ <string name="sepia_effect_off">Sepia Off</string>
+
+ <string-array name="num_of_folds_array">
+ <item>2</item>
+ <item>3</item>
+ <item>4</item>
+ <item>5</item>
+ <item>6</item>
+ <item>7</item>
+ <item>8</item>
+ <item>1</item>
+ </string-array>
+
+</resources>
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 0000000..8afb27e
--- /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 0000000..1a1033d
--- /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 0000000..a305568
--- /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();
+}