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.android.camera.widget; 18 19 import android.animation.Animator; 20 import android.animation.TimeInterpolator; 21 import android.animation.ValueAnimator; 22 import android.content.Context; 23 import android.graphics.Canvas; 24 import android.graphics.ColorFilter; 25 import android.graphics.Paint; 26 import android.graphics.PixelFormat; 27 import android.graphics.drawable.Drawable; 28 import android.os.Handler; 29 import android.os.Looper; 30 import android.util.AttributeSet; 31 import android.view.MotionEvent; 32 import android.view.View; 33 import android.view.animation.AnimationUtils; 34 import android.widget.FrameLayout; 35 36 import com.android.camera.filmstrip.FilmstripContentPanel; 37 import com.android.camera.filmstrip.FilmstripController; 38 import com.android.camera.ui.FilmstripGestureRecognizer; 39 import com.android.camera.util.ApiHelper; 40 import com.android.camera.util.Gusterpolator; 41 import com.android.camera2.R; 42 43 /** 44 * A {@link android.widget.FrameLayout} used for the parent layout of a 45 * {@link com.android.camera.widget.FilmstripView} to support animating in/out the 46 * filmstrip. 47 */ 48 public class FilmstripLayout extends FrameLayout implements FilmstripContentPanel { 49 50 private static final long DEFAULT_DURATION_MS = 250; 51 /** 52 * If the fling velocity exceeds this threshold, open filmstrip at a constant 53 * speed. Unit: pixel/ms. 54 */ 55 private static final float FLING_VELOCITY_THRESHOLD = 4.0f; 56 57 /** 58 * The layout containing the {@link com.android.camera.widget.FilmstripView} 59 * and other controls. 60 */ 61 private FrameLayout mFilmstripContentLayout; 62 private FilmstripView mFilmstripView; 63 private FilmstripGestureRecognizer mGestureRecognizer; 64 private FilmstripGestureRecognizer.Listener mFilmstripGestureListener; 65 private final ValueAnimator mFilmstripAnimator = ValueAnimator.ofFloat(null); 66 private int mSwipeTrend; 67 private FilmstripBackground mBackgroundDrawable; 68 private Handler mHandler; 69 // We use this to record the current translation position instead of using 70 // the real value because we might set the translation before onMeasure() 71 // thus getMeasuredWidth() can be 0. 72 private float mFilmstripContentTranslationProgress; 73 74 private Animator.AnimatorListener mFilmstripAnimatorListener = new Animator.AnimatorListener() { 75 private boolean mCanceled; 76 77 @Override 78 public void onAnimationStart(Animator animator) { 79 mCanceled = false; 80 } 81 82 @Override 83 public void onAnimationEnd(Animator animator) { 84 if (!mCanceled) { 85 if (mFilmstripContentTranslationProgress != 0f) { 86 mFilmstripView.getController().goToFilmstrip(); 87 setVisibility(INVISIBLE); 88 } else { 89 notifyShown(); 90 } 91 } 92 } 93 94 @Override 95 public void onAnimationCancel(Animator animator) { 96 mCanceled = true; 97 } 98 99 @Override 100 public void onAnimationRepeat(Animator animator) { 101 // Nothing. 102 } 103 }; 104 105 private ValueAnimator.AnimatorUpdateListener mFilmstripAnimatorUpdateListener = 106 new ValueAnimator.AnimatorUpdateListener() { 107 @Override 108 public void onAnimationUpdate(ValueAnimator valueAnimator) { 109 translateContentLayout((Float) valueAnimator.getAnimatedValue()); 110 mBackgroundDrawable.invalidateSelf(); 111 } 112 }; 113 private Listener mListener; 114 FilmstripLayout(Context context)115 public FilmstripLayout(Context context) { 116 super(context); 117 init(context); 118 } 119 FilmstripLayout(Context context, AttributeSet attrs)120 public FilmstripLayout(Context context, AttributeSet attrs) { 121 super(context, attrs); 122 init(context); 123 } 124 FilmstripLayout(Context context, AttributeSet attrs, int defStyle)125 public FilmstripLayout(Context context, AttributeSet attrs, int defStyle) { 126 super(context, attrs, defStyle); 127 init(context); 128 } 129 init(Context context)130 private void init(Context context) { 131 mGestureRecognizer = new FilmstripGestureRecognizer(context, new OpenFilmstripGesture()); 132 mFilmstripAnimator.setDuration(DEFAULT_DURATION_MS); 133 TimeInterpolator interpolator; 134 if (ApiHelper.isLOrHigher()) { 135 interpolator = AnimationUtils.loadInterpolator( 136 getContext(), android.R.interpolator.fast_out_slow_in); 137 } else { 138 interpolator = Gusterpolator.INSTANCE; 139 } 140 mFilmstripAnimator.setInterpolator(interpolator); 141 mFilmstripAnimator.addUpdateListener(mFilmstripAnimatorUpdateListener); 142 mFilmstripAnimator.addListener(mFilmstripAnimatorListener); 143 mHandler = new Handler(Looper.getMainLooper()); 144 mBackgroundDrawable = new FilmstripBackground(); 145 mBackgroundDrawable.setCallback(new Drawable.Callback() { 146 @Override 147 public void invalidateDrawable(Drawable drawable) { 148 FilmstripLayout.this.invalidate(); 149 } 150 151 @Override 152 public void scheduleDrawable(Drawable drawable, Runnable runnable, long l) { 153 mHandler.postAtTime(runnable, drawable, l); 154 } 155 156 @Override 157 public void unscheduleDrawable(Drawable drawable, Runnable runnable) { 158 mHandler.removeCallbacks(runnable, drawable); 159 } 160 }); 161 setBackground(mBackgroundDrawable); 162 } 163 164 @Override setFilmstripListener(Listener listener)165 public void setFilmstripListener(Listener listener) { 166 mListener = listener; 167 if (getVisibility() == VISIBLE && mFilmstripContentTranslationProgress == 0f) { 168 notifyShown(); 169 } else { 170 if (getVisibility() != VISIBLE) { 171 notifyHidden(); 172 } 173 } 174 mFilmstripView.getController().setListener(listener); 175 } 176 177 @Override hide()178 public void hide() { 179 translateContentLayout(1f); 180 mFilmstripAnimatorListener.onAnimationEnd(mFilmstripAnimator); 181 } 182 183 @Override show()184 public void show() { 185 translateContentLayout(0f); 186 mFilmstripAnimatorListener.onAnimationEnd(mFilmstripAnimator); 187 } 188 189 @Override setVisibility(int visibility)190 public void setVisibility(int visibility) { 191 super.setVisibility(visibility); 192 if (visibility != VISIBLE) { 193 notifyHidden(); 194 } 195 } 196 notifyHidden()197 private void notifyHidden() { 198 if (mListener == null) { 199 return; 200 } 201 mListener.onFilmstripHidden(); 202 } 203 notifyShown()204 private void notifyShown() { 205 if (mListener == null) { 206 return; 207 } 208 mListener.onFilmstripShown(); 209 mFilmstripView.zoomAtIndexChanged(); 210 FilmstripController controller = mFilmstripView.getController(); 211 int currentId = controller.getCurrentAdapterIndex(); 212 if (controller.inFilmstrip()) { 213 mListener.onEnterFilmstrip(currentId); 214 } else if (controller.inFullScreen()) { 215 mListener.onEnterFullScreenUiShown(currentId); 216 } 217 } 218 219 @Override onLayout(boolean changed, int l, int t, int r, int b)220 public void onLayout(boolean changed, int l, int t, int r, int b) { 221 super.onLayout(changed, l, t, r, b); 222 if (changed && mFilmstripView != null && getVisibility() == INVISIBLE) { 223 hide(); 224 } else { 225 translateContentLayout(mFilmstripContentTranslationProgress); 226 } 227 } 228 229 @Override onTouchEvent(MotionEvent ev)230 public boolean onTouchEvent(MotionEvent ev) { 231 return mGestureRecognizer.onTouchEvent(ev); 232 } 233 234 @Override onInterceptTouchEvent(MotionEvent ev)235 public boolean onInterceptTouchEvent(MotionEvent ev) { 236 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { 237 // TODO: Remove this after the touch flow refactor is done in 238 // MainAtivityLayout. 239 getParent().requestDisallowInterceptTouchEvent(true); 240 } 241 return false; 242 } 243 244 @Override onFinishInflate()245 public void onFinishInflate() { 246 mFilmstripView = (FilmstripView) findViewById(R.id.filmstrip_view); 247 mFilmstripView.setOnTouchListener(new OnTouchListener() { 248 249 @Override 250 public boolean onTouch(View view, MotionEvent motionEvent) { 251 // Adjust the coordinates back since they are relative to the 252 // child view. 253 motionEvent.setLocation(motionEvent.getX() + mFilmstripContentLayout.getX(), 254 motionEvent.getY() + mFilmstripContentLayout.getY()); 255 mGestureRecognizer.onTouchEvent(motionEvent); 256 return true; 257 } 258 }); 259 mFilmstripGestureListener = mFilmstripView.getGestureListener(); 260 mFilmstripContentLayout = (FrameLayout) findViewById(R.id.camera_filmstrip_content_layout); 261 } 262 263 @Override onBackPressed()264 public boolean onBackPressed() { 265 return animateHide(); 266 } 267 268 @Override animateHide()269 public boolean animateHide() { 270 if (getVisibility() == VISIBLE) { 271 if (!mFilmstripAnimator.isRunning()) { 272 hideFilmstrip(); 273 } 274 return true; 275 } 276 return false; 277 } 278 hideFilmstrip()279 public void hideFilmstrip() { 280 // run the same view show/hides and animations 281 // that happen with a swipe gesture. 282 onSwipeOutBegin(); 283 runAnimation(mFilmstripContentTranslationProgress, 1f); 284 } 285 showFilmstrip()286 public void showFilmstrip() { 287 setVisibility(VISIBLE); 288 runAnimation(mFilmstripContentTranslationProgress, 0f); 289 } 290 runAnimation(float begin, float end)291 private void runAnimation(float begin, float end) { 292 if (mFilmstripAnimator.isRunning()) { 293 return; 294 } 295 if (begin == end) { 296 // No need to start animation. 297 mFilmstripAnimatorListener.onAnimationEnd(mFilmstripAnimator); 298 return; 299 } 300 mFilmstripAnimator.setFloatValues(begin, end); 301 mFilmstripAnimator.start(); 302 } 303 translateContentLayout(float fraction)304 private void translateContentLayout(float fraction) { 305 mFilmstripContentTranslationProgress = fraction; 306 mFilmstripContentLayout.setTranslationX(fraction * getMeasuredWidth()); 307 } 308 translateContentLayoutByPixel(float pixel)309 private void translateContentLayoutByPixel(float pixel) { 310 mFilmstripContentLayout.setTranslationX(pixel); 311 mFilmstripContentTranslationProgress = pixel / getMeasuredWidth(); 312 } 313 onSwipeOut()314 private void onSwipeOut() { 315 if (mListener != null) { 316 mListener.onSwipeOut(); 317 } 318 } 319 onSwipeOutBegin()320 private void onSwipeOutBegin() { 321 if (mListener != null) { 322 mListener.onSwipeOutBegin(); 323 } 324 } 325 326 /** 327 * A gesture listener which passes all the gestures to the 328 * {@code mFilmstripView} by default and only intercepts scroll gestures 329 * when the {@code mFilmstripView} is not in full-screen. 330 */ 331 private class OpenFilmstripGesture implements FilmstripGestureRecognizer.Listener { 332 @Override onScroll(float x, float y, float dx, float dy)333 public boolean onScroll(float x, float y, float dx, float dy) { 334 if (mFilmstripView.getController().getCurrentAdapterIndex() == -1) { 335 return true; 336 } 337 if (mFilmstripAnimator.isRunning()) { 338 return true; 339 } 340 if (mFilmstripContentLayout.getTranslationX() == 0f && 341 mFilmstripGestureListener.onScroll(x, y, dx, dy)) { 342 return true; 343 } 344 mSwipeTrend = (((int) dx) >> 1) + (mSwipeTrend >> 1); 345 if (dx < 0 && mFilmstripContentLayout.getTranslationX() == 0) { 346 mBackgroundDrawable.setOffset(0); 347 FilmstripLayout.this.onSwipeOutBegin(); 348 } 349 350 // When we start translating the filmstrip in, we want the left edge of the 351 // first view to always be at the rightmost edge of the screen so that it 352 // appears instantly, regardless of the view's distance from the edge of the 353 // filmstrip view. To do so, on our first translation, jump the filmstrip view 354 // to the correct position, and then smoothly animate the translation from that 355 // initial point. 356 if (dx > 0 && mFilmstripContentLayout.getTranslationX() == getMeasuredWidth()) { 357 final int currentItemLeft = mFilmstripView.getCurrentItemLeft(); 358 dx = currentItemLeft; 359 mBackgroundDrawable.setOffset(currentItemLeft); 360 } 361 362 float translate = mFilmstripContentLayout.getTranslationX() - dx; 363 if (translate < 0f) { 364 translate = 0f; 365 } else { 366 if (translate > getMeasuredWidth()) { 367 translate = getMeasuredWidth(); 368 } 369 } 370 translateContentLayoutByPixel(translate); 371 if (translate == 0 && dx > 0) { 372 // This will only happen once since when this condition holds 373 // the onScroll() callback will be forwarded to the filmstrip 374 // view. 375 mFilmstripAnimatorListener.onAnimationEnd(mFilmstripAnimator); 376 } 377 mBackgroundDrawable.invalidateSelf(); 378 return true; 379 } 380 381 @Override onMouseScroll(float hscroll, float vscroll)382 public boolean onMouseScroll(float hscroll, float vscroll) { 383 if (mFilmstripContentTranslationProgress == 0f) { 384 return mFilmstripGestureListener.onMouseScroll(hscroll, vscroll); 385 } 386 return false; 387 } 388 389 @Override onSingleTapUp(float x, float y)390 public boolean onSingleTapUp(float x, float y) { 391 if (mFilmstripContentTranslationProgress == 0f) { 392 return mFilmstripGestureListener.onSingleTapUp(x, y); 393 } 394 return false; 395 } 396 397 @Override onDoubleTap(float x, float y)398 public boolean onDoubleTap(float x, float y) { 399 if (mFilmstripContentTranslationProgress == 0f) { 400 return mFilmstripGestureListener.onDoubleTap(x, y); 401 } 402 return false; 403 } 404 405 /** 406 * @param velocityX The fling velocity in the X direction. 407 * @return Whether the filmstrip should be opened, 408 * given velocityX and mSwipeTrend. 409 */ flingShouldOpenFilmstrip(float velocityX)410 private boolean flingShouldOpenFilmstrip(float velocityX) { 411 return (mSwipeTrend > 0) && 412 (velocityX < 0.0f) && 413 (Math.abs(velocityX / 1000.0f) > FLING_VELOCITY_THRESHOLD); 414 } 415 416 @Override onFling(float velocityX, float velocityY)417 public boolean onFling(float velocityX, float velocityY) { 418 if (mFilmstripContentTranslationProgress == 0f) { 419 return mFilmstripGestureListener.onFling(velocityX, velocityY); 420 } else if (flingShouldOpenFilmstrip(velocityX)) { 421 showFilmstrip(); 422 return true; 423 } 424 425 return false; 426 } 427 428 @Override onScaleBegin(float focusX, float focusY)429 public boolean onScaleBegin(float focusX, float focusY) { 430 if (mFilmstripContentTranslationProgress == 0f) { 431 return mFilmstripGestureListener.onScaleBegin(focusX, focusY); 432 } 433 return false; 434 } 435 436 @Override onScale(float focusX, float focusY, float scale)437 public boolean onScale(float focusX, float focusY, float scale) { 438 if (mFilmstripContentTranslationProgress == 0f) { 439 return mFilmstripGestureListener.onScale(focusX, focusY, scale); 440 } 441 return false; 442 } 443 444 @Override onDown(float x, float y)445 public boolean onDown(float x, float y) { 446 if (mFilmstripContentLayout.getTranslationX() == 0f) { 447 return mFilmstripGestureListener.onDown(x, y); 448 } 449 return false; 450 } 451 452 @Override onUp(float x, float y)453 public boolean onUp(float x, float y) { 454 if (mFilmstripContentLayout.getTranslationX() == 0f) { 455 return mFilmstripGestureListener.onUp(x, y); 456 } 457 if (mSwipeTrend < 0) { 458 hideFilmstrip(); 459 onSwipeOut(); 460 } else { 461 if (mFilmstripContentLayout.getTranslationX() >= getMeasuredWidth() / 2) { 462 hideFilmstrip(); 463 onSwipeOut(); 464 } else { 465 showFilmstrip(); 466 } 467 } 468 mSwipeTrend = 0; 469 return false; 470 } 471 472 @Override onLongPress(float x, float y)473 public void onLongPress(float x, float y) { 474 mFilmstripGestureListener.onLongPress(x, y); 475 } 476 477 @Override onScaleEnd()478 public void onScaleEnd() { 479 if (mFilmstripContentLayout.getTranslationX() == 0f) { 480 mFilmstripGestureListener.onScaleEnd(); 481 } 482 } 483 } 484 485 private class FilmstripBackground extends Drawable { 486 private Paint mPaint; 487 private int mOffset; 488 FilmstripBackground()489 public FilmstripBackground() { 490 mPaint = new Paint(); 491 mPaint.setAntiAlias(true); 492 mPaint.setColor(getResources().getColor(R.color.camera_gray_background)); 493 mPaint.setAlpha(255); 494 } 495 496 /** 497 * Adjust the target width and translation calculation when we start translating 498 * from a point where width != translationX so that alpha scales smoothly. 499 */ setOffset(int offset)500 public void setOffset(int offset) { 501 mOffset = offset; 502 } 503 504 @Override setAlpha(int i)505 public void setAlpha(int i) { 506 mPaint.setAlpha(i); 507 } 508 setAlpha(float a)509 private void setAlpha(float a) { 510 setAlpha((int) (a*255.0f)); 511 } 512 513 @Override setColorFilter(ColorFilter colorFilter)514 public void setColorFilter(ColorFilter colorFilter) { 515 mPaint.setColorFilter(colorFilter); 516 } 517 518 @Override getOpacity()519 public int getOpacity() { 520 return PixelFormat.TRANSLUCENT; 521 } 522 523 @Override draw(Canvas canvas)524 public void draw(Canvas canvas) { 525 int width = getMeasuredWidth() - mOffset; 526 float translation = mFilmstripContentLayout.getTranslationX() - mOffset; 527 if (translation == width) { 528 return; 529 } 530 531 setAlpha(1.0f - mFilmstripContentTranslationProgress); 532 canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint); 533 } 534 } 535 } 536