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.animation.ObjectAnimator;
20 import android.animation.ValueAnimator;
21 import android.app.Activity;
22 import android.graphics.Color;
23 import android.graphics.ColorMatrix;
24 import android.graphics.ColorMatrixColorFilter;
25 import android.graphics.Paint;
26 import android.graphics.SurfaceTexture;
27 import android.hardware.Camera;
28 import android.os.Build;
29 import android.os.Bundle;
30 import android.view.GestureDetector;
31 import android.view.Menu;
32 import android.view.MenuItem;
33 import android.view.MotionEvent;
34 import android.view.TextureView;
35 import android.view.View;
36 import android.view.ViewConfiguration;
37 import android.view.ViewGroup;
38 import android.view.animation.AccelerateInterpolator;
39 import android.widget.AdapterView;
40 import android.widget.AdapterView.OnItemSelectedListener;
41 import android.widget.ImageView;
42 import android.widget.SeekBar;
43 import android.widget.Spinner;
44 
45 import com.example.android.foldinglayout.FoldingLayout.Orientation;
46 
47 import java.io.IOException;
48 
49 /**
50  * This application creates  a paper like folding effect of some view.
51  * The number of folds, orientation (vertical or horizontal) of the fold, and the
52  * anchor point about which the view will fold can be set to achieve different
53  * folding effects.
54  *
55  * Using bitmap and canvas scaling techniques, the foldingLayout can be scaled so as
56  * to depict a paper-like folding effect. The addition of shadows on the separate folds
57  * adds a sense of realism to the visual effect.
58  *
59  * This application shows folding of a TextureView containing a live camera feed,
60  * as well as the folding of an ImageView with a static image. The TextureView experiences
61  * jagged edges as a result of scaling operations on rectangles. The ImageView however
62  * contains a 1 pixel transparent border around its contents which can be used to avoid
63  * this unwanted artifact.
64  */
65 public class FoldingLayoutActivity extends Activity {
66 
67     private final int ANTIALIAS_PADDING = 1;
68 
69     private final int FOLD_ANIMATION_DURATION = 1000;
70 
71     /* A bug was introduced in Android 4.3 that ignores changes to the Canvas state
72      * between multiple calls to super.dispatchDraw() when running with hardware acceleration.
73      * To account for this bug, a slightly different approach was taken to fold a
74      * static image whereby a bitmap of the original contents is captured and drawn
75      * in segments onto the canvas. However, this method does not permit the folding
76      * of a TextureView hosting a live camera feed which continuously updates.
77      * Furthermore, the sepia effect was removed from the bitmap variation of the
78      * demo to simplify the logic when running with this workaround."
79      */
80     static final boolean IS_JBMR2 = Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR2;
81 
82     private FoldingLayout mFoldLayout;
83     private SeekBar mAnchorSeekBar;
84     private Orientation mOrientation = Orientation.HORIZONTAL;
85 
86     private int mTranslation = 0;
87     private int mNumberOfFolds = 2;
88     private int mParentPositionY = -1;
89     private int mTouchSlop = -1;
90 
91     private float mAnchorFactor = 0;
92 
93     private boolean mDidLoadSpinner = true;
94     private boolean mDidNotStartScroll = true;
95 
96     private boolean mIsCameraFeed = false;
97     private boolean mIsSepiaOn = true;
98 
99     private GestureDetector mScrollGestureDetector;
100     private ItemSelectedListener mItemSelectedListener;
101 
102     private Camera mCamera;
103     private TextureView mTextureView;
104     private ImageView mImageView;
105 
106     private Paint mSepiaPaint;
107     private Paint mDefaultPaint;
108 
109     @Override
onCreate(Bundle savedInstanceState)110     protected void onCreate(Bundle savedInstanceState) {
111         super.onCreate(savedInstanceState);
112 
113         setContentView(R.layout.activity_fold);
114 
115         mImageView = (ImageView)findViewById(R.id.image_view);
116         mImageView.setPadding(ANTIALIAS_PADDING, ANTIALIAS_PADDING, ANTIALIAS_PADDING,
117                 ANTIALIAS_PADDING);
118         mImageView.setScaleType(ImageView.ScaleType.FIT_XY);
119         mImageView.setImageDrawable(getResources().getDrawable(R.drawable.image));
120 
121         mTextureView = new TextureView(this);
122         mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
123 
124         mAnchorSeekBar = (SeekBar)findViewById(R.id.anchor_seek_bar);
125         mFoldLayout = (FoldingLayout)findViewById(R.id.fold_view);
126         mFoldLayout.setBackgroundColor(Color.BLACK);
127         mFoldLayout.setFoldListener(mOnFoldListener);
128 
129         mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
130 
131         mAnchorSeekBar.setOnSeekBarChangeListener(mSeekBarChangeListener);
132 
133         mScrollGestureDetector = new GestureDetector(this, new ScrollGestureDetector());
134         mItemSelectedListener = new ItemSelectedListener();
135 
136         mDefaultPaint = new Paint();
137         mSepiaPaint = new Paint();
138 
139         ColorMatrix m1 = new ColorMatrix();
140         ColorMatrix m2 = new ColorMatrix();
141         m1.setSaturation(0);
142         m2.setScale(1f, .95f, .82f, 1.0f);
143         m1.setConcat(m2, m1);
144         mSepiaPaint.setColorFilter(new ColorMatrixColorFilter(m1));
145     }
146 
147     /**
148      * This listener, along with the setSepiaLayer method below, show a possible use case
149      * of the OnFoldListener provided with the FoldingLayout. This is a fun extra addition
150      * to the demo showing what kind of visual effects can be applied to the child of the
151      * FoldingLayout by setting the layer type to hardware. With a hardware layer type
152      * applied to the child, a paint object can also be applied to the same layer. Using
153      * the concatenation of two different color matrices (above), a color filter was created
154      * which simulates a sepia effect on the layer.*/
155     private OnFoldListener mOnFoldListener =
156             new OnFoldListener() {
157         @Override
158         public void onStartFold() {
159             if (mIsSepiaOn) {
160                 setSepiaLayer(mFoldLayout.getChildAt(0), true);
161             }
162         }
163 
164         @Override
165         public void onEndFold() {
166             setSepiaLayer(mFoldLayout.getChildAt(0), false);
167         }
168     };
169 
setSepiaLayer(View view, boolean isSepiaLayerOn)170     private void setSepiaLayer (View view, boolean isSepiaLayerOn) {
171         if (!IS_JBMR2) {
172             if (isSepiaLayerOn) {
173                 view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
174                 view.setLayerPaint(mSepiaPaint);
175             } else {
176                 view.setLayerPaint(mDefaultPaint);
177             }
178         }
179     }
180 
181     /**
182      * Creates a SurfaceTextureListener in order to prepare a TextureView
183      * which displays a live, and continuously updated, feed from the Camera.
184      */
185     private TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView
186             .SurfaceTextureListener() {
187         @Override
188         public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i2) {
189             mCamera = Camera.open();
190 
191             if (mCamera == null && Camera.getNumberOfCameras() > 1) {
192                 mCamera = mCamera.open(Camera.CameraInfo.CAMERA_FACING_FRONT);
193             }
194 
195             if (mCamera == null) {
196                 return;
197             }
198 
199             try {
200                 mCamera.setPreviewTexture(surfaceTexture);
201                 mCamera.setDisplayOrientation(90);
202                 mCamera.startPreview();
203             } catch (IOException e) {
204                 e.printStackTrace();
205             }
206         }
207 
208         @Override
209         public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i2) {
210             // Ignored, Camera does all the work for us
211         }
212 
213         @Override
214         public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
215             if (mCamera != null) {
216                 mCamera.stopPreview();
217                 mCamera.release();
218             }
219             return true;
220         }
221 
222         @Override
223         public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
224             // Invoked every time there's a new Camera preview frame
225         }
226     };
227 
228     /**
229      * A listener for scrolling changes in the seekbar. The anchor point of the folding
230      * view is updated every time the seekbar stops tracking touch events. Every time the
231      * anchor point is updated, the folding view is restored to a default unfolded state.
232      */
233     private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener = new SeekBar
234             .OnSeekBarChangeListener() {
235         @Override
236         public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
237         }
238 
239         @Override
240         public void onStartTrackingTouch(SeekBar seekBar) {
241         }
242 
243         @Override
244         public void onStopTrackingTouch(SeekBar seekBar) {
245             mTranslation = 0;
246             mAnchorFactor = ((float)mAnchorSeekBar.getProgress())/100.0f;
247             mFoldLayout.setAnchorFactor(mAnchorFactor);
248         }
249     };
250 
251     @Override
onCreateOptionsMenu(Menu menu)252     public boolean onCreateOptionsMenu(Menu menu) {
253         if (IS_JBMR2) {
254             getMenuInflater().inflate(R.menu.fold_with_bug, menu);
255         } else {
256             getMenuInflater().inflate(R.menu.fold, menu);
257         }
258         Spinner s = (Spinner) menu.findItem(R.id.num_of_folds).getActionView();
259         s.setOnItemSelectedListener(mItemSelectedListener);
260         return true;
261     }
262 
263     @Override
onWindowFocusChanged(boolean hasFocus)264     public void onWindowFocusChanged (boolean hasFocus) {
265         super.onWindowFocusChanged(hasFocus);
266 
267         int[] loc = new int[2];
268         mFoldLayout.getLocationOnScreen(loc);
269         mParentPositionY = loc[1];
270     }
271 
272     @Override
onTouchEvent(MotionEvent me)273     public boolean onTouchEvent(MotionEvent me) {
274         return mScrollGestureDetector.onTouchEvent(me);
275     }
276 
277     @Override
onOptionsItemSelected(MenuItem item)278     public boolean onOptionsItemSelected (MenuItem item) {
279         switch(item.getItemId()) {
280             case R.id.animate_fold:
281                 animateFold();
282                 break;
283             case R.id.toggle_orientation:
284                 mOrientation = (mOrientation == Orientation.HORIZONTAL) ? Orientation.VERTICAL :
285                         Orientation.HORIZONTAL;
286                 item.setTitle((mOrientation == Orientation.HORIZONTAL) ? R.string.vertical :
287                 R.string.horizontal);
288                 mTranslation = 0;
289                 mFoldLayout.setOrientation(mOrientation);
290                 break;
291             case R.id.camera_feed:
292                 mIsCameraFeed = !mIsCameraFeed;
293                 item.setTitle(mIsCameraFeed ? R.string.static_image : R.string.camera_feed);
294                 item.setChecked(mIsCameraFeed);
295                 if (mIsCameraFeed) {
296                     mFoldLayout.removeView(mImageView);
297                     mFoldLayout.addView(mTextureView, new ViewGroup.LayoutParams(
298                             mFoldLayout.getWidth(), mFoldLayout.getHeight()));
299                 } else {
300                     mFoldLayout.removeView(mTextureView);
301                     mFoldLayout.addView(mImageView, new ViewGroup.LayoutParams(
302                             mFoldLayout.getWidth(), mFoldLayout.getHeight()));
303                 }
304                 mTranslation = 0;
305                 break;
306             case R.id.sepia:
307                 mIsSepiaOn = !mIsSepiaOn;
308                 item.setChecked(!mIsSepiaOn);
309                 if (mIsSepiaOn && mFoldLayout.getFoldFactor() != 0) {
310                     setSepiaLayer(mFoldLayout.getChildAt(0), true);
311                 } else {
312                     setSepiaLayer(mFoldLayout.getChildAt(0), false);
313                 }
314                 break;
315             default:
316                break;
317 
318         }
319         return super.onOptionsItemSelected(item);
320     }
321 
322     /**
323      * Animates the folding view inwards (to a completely folded state) from its
324      * current state and then back out to its original state.
325      */
animateFold()326     public void animateFold ()
327     {
328         float foldFactor = mFoldLayout.getFoldFactor();
329 
330         ObjectAnimator animator = ObjectAnimator.ofFloat(mFoldLayout, "foldFactor", foldFactor, 1);
331         animator.setRepeatMode(ValueAnimator.REVERSE);
332         animator.setRepeatCount(1);
333         animator.setDuration(FOLD_ANIMATION_DURATION);
334         animator.setInterpolator(new AccelerateInterpolator());
335         animator.start();
336     }
337 
338     /**
339      * Listens for selection events of the spinner located on the action bar. Every
340      * time a new value is selected, the number of folds in the folding view is updated
341      * and is also restored to a default unfolded state.
342      */
343     private class ItemSelectedListener implements OnItemSelectedListener {
344         @Override
onItemSelected(AdapterView<?> parent, View view, int pos, long id)345         public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
346             mNumberOfFolds = Integer.parseInt(parent.getItemAtPosition(pos).toString());
347             if (mDidLoadSpinner) {
348                 mDidLoadSpinner = false;
349             } else {
350                 mTranslation = 0;
351                 mFoldLayout.setNumberOfFolds(mNumberOfFolds);
352             }
353         }
354 
355         @Override
onNothingSelected(AdapterView<?> arg0)356         public void onNothingSelected(AdapterView<?> arg0) {
357         }
358     }
359 
360     /** This class uses user touch events to fold and unfold the folding view. */
361     private class ScrollGestureDetector extends GestureDetector.SimpleOnGestureListener {
362         @Override
onDown(MotionEvent e)363         public boolean onDown (MotionEvent e) {
364             mDidNotStartScroll = true;
365             return true;
366         }
367 
368         /**
369          * All the logic here is used to determine by what factor the paper view should
370          * be folded in response to the user's touch events. The logic here uses vertical
371          * scrolling to fold a vertically oriented view and horizontal scrolling to fold
372          * a horizontally oriented fold. Depending on where the anchor point of the fold is,
373          * movements towards or away from the anchor point will either fold or unfold
374          * the paper respectively.
375          *
376          * The translation logic here also accounts for the touch slop when a new user touch
377          * begins, but before a scroll event is first invoked.
378          */
379         @Override
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)380         public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
381             int touchSlop = 0;
382             float factor;
383             if (mOrientation == Orientation.VERTICAL) {
384                 factor = Math.abs((float)(mTranslation) / (float)(mFoldLayout.getHeight()));
385 
386                 if (e2.getY() - mParentPositionY <= mFoldLayout.getHeight()
387                         && e2.getY() - mParentPositionY >= 0) {
388                     if ((e2.getY() - mParentPositionY) > mFoldLayout.getHeight() * mAnchorFactor) {
389                         mTranslation -= (int)distanceY;
390                         touchSlop = distanceY < 0 ? -mTouchSlop : mTouchSlop;
391                     } else {
392                         mTranslation += (int)distanceY;
393                         touchSlop = distanceY < 0 ? mTouchSlop : -mTouchSlop;
394                     }
395                     mTranslation = mDidNotStartScroll ? mTranslation + touchSlop : mTranslation;
396 
397                     if (mTranslation < -mFoldLayout.getHeight()) {
398                         mTranslation = -mFoldLayout.getHeight();
399                     }
400                 }
401             } else {
402                 factor = Math.abs(((float)mTranslation) / ((float) mFoldLayout.getWidth()));
403 
404                 if (e2.getRawX() > mFoldLayout.getWidth() * mAnchorFactor) {
405                     mTranslation -= (int)distanceX;
406                     touchSlop = distanceX < 0 ? -mTouchSlop : mTouchSlop;
407                 } else {
408                     mTranslation += (int)distanceX;
409                     touchSlop = distanceX < 0 ? mTouchSlop : -mTouchSlop;
410                 }
411                 mTranslation = mDidNotStartScroll ? mTranslation + touchSlop : mTranslation;
412 
413                 if (mTranslation < -mFoldLayout.getWidth()) {
414                     mTranslation = -mFoldLayout.getWidth();
415                 }
416             }
417 
418             mDidNotStartScroll = false;
419 
420             if (mTranslation > 0) {
421                 mTranslation = 0;
422             }
423 
424             mFoldLayout.setFoldFactor(factor);
425 
426             return true;
427         }
428     }
429 }