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