Home | History | Annotate | Download | only in foldinglayout
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.example.android.foldinglayout;
     18 
     19 import android.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
    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 
    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
    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
    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
    273     public boolean onTouchEvent(MotionEvent me) {
    274         return mScrollGestureDetector.onTouchEvent(me);
    275     }
    276 
    277     @Override
    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      */
    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
    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
    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
    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
    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 }