blob: 1a1033dcb207a53cc5dd31450b00166e3aae2a36 [file] [log] [blame]
Daniel Olshansky371fcc22013-07-02 15:12:05 -07001/*
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
17package com.example.android.foldinglayout;
18
19import android.animation.ObjectAnimator;
20import android.animation.ValueAnimator;
21import android.app.Activity;
22import android.graphics.Color;
23import android.graphics.ColorMatrix;
24import android.graphics.ColorMatrixColorFilter;
25import android.graphics.Paint;
26import android.graphics.SurfaceTexture;
27import android.hardware.Camera;
28import android.os.Build;
29import android.os.Bundle;
30import android.view.GestureDetector;
31import android.view.Menu;
32import android.view.MenuItem;
33import android.view.MotionEvent;
34import android.view.TextureView;
35import android.view.View;
36import android.view.ViewConfiguration;
37import android.view.ViewGroup;
38import android.view.animation.AccelerateInterpolator;
39import android.widget.AdapterView;
40import android.widget.AdapterView.OnItemSelectedListener;
41import android.widget.ImageView;
42import android.widget.SeekBar;
43import android.widget.Spinner;
44
45import com.example.android.foldinglayout.FoldingLayout.Orientation;
46
47import 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 */
65public 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}