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 }