/* * 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; } } }