FilmstripView.java revision 1f9db2dfe59c33228103c54523281501ef52c9ba
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 17package com.android.camera.widget; 18 19import android.animation.Animator; 20import android.animation.AnimatorSet; 21import android.animation.TimeInterpolator; 22import android.animation.ValueAnimator; 23import android.content.Context; 24import android.graphics.Canvas; 25import android.graphics.Rect; 26import android.graphics.RectF; 27import android.net.Uri; 28import android.os.Handler; 29import android.util.AttributeSet; 30import android.util.DisplayMetrics; 31import android.util.Log; 32import android.view.MotionEvent; 33import android.view.View; 34import android.view.ViewGroup; 35import android.view.animation.DecelerateInterpolator; 36import android.widget.Scroller; 37 38import com.android.camera.CameraActivity; 39import com.android.camera.filmstrip.DataAdapter; 40import com.android.camera.filmstrip.FilmstripController; 41import com.android.camera.filmstrip.ImageData; 42import com.android.camera.ui.FilmstripGestureRecognizer; 43import com.android.camera.ui.ZoomView; 44import com.android.camera.util.UsageStatistics; 45import com.android.camera2.R; 46 47import java.util.Arrays; 48 49public class FilmstripView extends ViewGroup { 50 private static final String TAG = "FilmStripView"; 51 52 private static final int BUFFER_SIZE = 5; 53 private static final int GEOMETRY_ADJUST_TIME_MS = 400; 54 private static final int SNAP_IN_CENTER_TIME_MS = 600; 55 private static final float FLING_COASTING_DURATION_S = 0.05f; 56 private static final int ZOOM_ANIMATION_DURATION_MS = 200; 57 private static final int CAMERA_PREVIEW_SWIPE_THRESHOLD = 300; 58 private static final float FILM_STRIP_SCALE = 0.7f; 59 private static final float FULL_SCREEN_SCALE = 1f; 60 61 private static final float TOLERANCE = 0.1f; 62 // Only check for intercepting touch events within first 500ms 63 private static final int SWIPE_TIME_OUT = 500; 64 private static final int DECELERATION_FACTOR = 4; 65 66 private CameraActivity mActivity; 67 private FilmstripGestureRecognizer mGestureRecognizer; 68 private FilmstripGestureRecognizer.Listener mGestureListener; 69 private DataAdapter mDataAdapter; 70 private int mViewGapInPixel; 71 private final Rect mDrawArea = new Rect(); 72 73 private final int mCurrentItem = (BUFFER_SIZE - 1) / 2; 74 private float mScale; 75 private MyController mController; 76 private int mCenterX = -1; 77 private final ViewItem[] mViewItem = new ViewItem[BUFFER_SIZE]; 78 79 private FilmstripController.FilmstripListener mListener; 80 private ZoomView mZoomView = null; 81 82 private MotionEvent mDown; 83 private boolean mCheckToIntercept = true; 84 private int mSlop; 85 private TimeInterpolator mViewAnimInterpolator; 86 87 // This is true if and only if the user is scrolling, 88 private boolean mIsUserScrolling; 89 private int mDataIdOnUserScrolling; 90 private float mOverScaleFactor = 1f; 91 92 /** 93 * A helper class to tract and calculate the view coordination. 94 */ 95 private class ViewItem { 96 private int mDataId; 97 /** The position of the left of the view in the whole filmstrip. */ 98 private int mLeftPosition; 99 private final View mView; 100 private final ImageData mData; 101 private final RectF mViewArea; 102 private boolean mMaximumBitmapRequested; 103 104 private ValueAnimator mTranslationXAnimator; 105 private ValueAnimator mTranslationYAnimator; 106 private ValueAnimator mAlphaAnimator; 107 108 /** 109 * Constructor. 110 * 111 * @param id The id of the data from 112 * {@link com.android.camera.filmstrip.DataAdapter}. 113 * @param v The {@code View} representing the data. 114 */ 115 public ViewItem(int id, View v, ImageData data) { 116 v.setPivotX(0f); 117 v.setPivotY(0f); 118 mDataId = id; 119 mData = data; 120 mView = v; 121 mMaximumBitmapRequested = false; 122 mLeftPosition = -1; 123 mViewArea = new RectF(); 124 } 125 126 public boolean isMaximumBitmapRequested() { 127 return mMaximumBitmapRequested; 128 } 129 130 public void setMaximumBitmapRequested() { 131 mMaximumBitmapRequested = true; 132 } 133 134 /** 135 * Returns the data id from 136 * {@link com.android.camera.filmstrip.DataAdapter}. 137 */ 138 public int getId() { 139 return mDataId; 140 } 141 142 /** 143 * Sets the data id from 144 * {@link com.android.camera.filmstrip.DataAdapter}. 145 */ 146 public void setId(int id) { 147 mDataId = id; 148 } 149 150 /** Sets the left position of the view in the whole filmstrip. */ 151 public void setLeftPosition(int pos) { 152 mLeftPosition = pos; 153 } 154 155 /** Returns the left position of the view in the whole filmstrip. */ 156 public int getLeftPosition() { 157 return mLeftPosition; 158 } 159 160 /** Returns the translation of Y regarding the view scale. */ 161 public float getTranslationY() { 162 return mView.getTranslationY() / mScale; 163 } 164 165 /** Returns the translation of X regarding the view scale. */ 166 public float getTranslationX() { 167 return mView.getTranslationX() / mScale; 168 } 169 170 /** Sets the translation of Y regarding the view scale. */ 171 public void setTranslationY(float transY) { 172 mView.setTranslationY(transY * mScale); 173 } 174 175 /** Sets the translation of X regarding the view scale. */ 176 public void setTranslationX(float transX) { 177 mView.setTranslationX(transX * mScale); 178 } 179 180 /** Forwarding of {@link android.view.View#setAlpha(float)}. */ 181 public void setAlpha(float alpha) { 182 mView.setAlpha(alpha); 183 } 184 185 /** Forwarding of {@link android.view.View#getAlpha()}. */ 186 public float getAlpha() { 187 return mView.getAlpha(); 188 } 189 190 /** Forwarding of {@link android.view.View#getMeasuredWidth()}. */ 191 public int getMeasuredWidth() { 192 return mView.getMeasuredWidth(); 193 } 194 195 /** 196 * Animates the X translation of the view. Note: the animated value is 197 * not set directly by {@link android.view.View#setTranslationX(float)} 198 * because the value might be changed during in {@code onLayout()}. 199 * The animated value of X translation is specially handled in {@code 200 * layoutIn()}. 201 * 202 * @param targetX The final value. 203 * @param duration_ms The duration of the animation. 204 * @param interpolator Time interpolator. 205 */ 206 public void animateTranslationX( 207 float targetX, long duration_ms, TimeInterpolator interpolator) { 208 if (mTranslationXAnimator == null) { 209 mTranslationXAnimator = new ValueAnimator(); 210 mTranslationXAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 211 @Override 212 public void onAnimationUpdate(ValueAnimator valueAnimator) { 213 // We invalidate the filmstrip view instead of setting the 214 // translation X because the translation X of the view is 215 // touched in onLayout(). See the documentation of 216 // animateTranslationX(). 217 invalidate(); 218 } 219 }); 220 } 221 runAnimation(mTranslationXAnimator, getTranslationX(), targetX, duration_ms, 222 interpolator); 223 } 224 225 /** 226 * Animates the Y translation of the view. 227 * 228 * @param targetY The final value. 229 * @param duration_ms The duration of the animation. 230 * @param interpolator Time interpolator. 231 */ 232 public void animateTranslationY( 233 float targetY, long duration_ms, TimeInterpolator interpolator) { 234 if (mTranslationYAnimator == null) { 235 mTranslationYAnimator = new ValueAnimator(); 236 mTranslationYAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 237 @Override 238 public void onAnimationUpdate(ValueAnimator valueAnimator) { 239 setTranslationY((Float) valueAnimator.getAnimatedValue()); 240 } 241 }); 242 } 243 runAnimation(mTranslationYAnimator, getTranslationY(), targetY, duration_ms, 244 interpolator); 245 } 246 247 /** 248 * Animates the alpha value of the view. 249 * 250 * @param targetAlpha The final value. 251 * @param duration_ms The duration of the animation. 252 * @param interpolator Time interpolator. 253 */ 254 public void animateAlpha(float targetAlpha, long duration_ms, 255 TimeInterpolator interpolator) { 256 if (mAlphaAnimator == null) { 257 mAlphaAnimator = new ValueAnimator(); 258 mAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 259 @Override 260 public void onAnimationUpdate(ValueAnimator valueAnimator) { 261 ViewItem.this.setAlpha((Float) valueAnimator.getAnimatedValue()); 262 } 263 }); 264 } 265 runAnimation(mAlphaAnimator, getAlpha(), targetAlpha, duration_ms, interpolator); 266 } 267 268 private void runAnimation(final ValueAnimator animator, final float startValue, 269 final float targetValue, final long duration_ms, 270 final TimeInterpolator interpolator) { 271 if (startValue == targetValue) { 272 return; 273 } 274 animator.setInterpolator(interpolator); 275 animator.setDuration(duration_ms); 276 animator.setFloatValues(startValue, targetValue); 277 animator.start(); 278 } 279 280 /** Adjusts the translation of X regarding the view scale. */ 281 public void translateXScaledBy(float transX) { 282 setTranslationX(getTranslationX() + transX * mScale); 283 } 284 285 /** 286 * Forwarding of {@link android.view.View#getHitRect(android.graphics.Rect)}. 287 */ 288 public void getHitRect(Rect rect) { 289 mView.getHitRect(rect); 290 } 291 292 public int getCenterX() { 293 return mLeftPosition + mView.getMeasuredWidth() / 2; 294 } 295 296 /** Forwarding of {@link android.view.View#getVisibility()}. */ 297 public int getVisibility() { 298 return mView.getVisibility(); 299 } 300 301 /** Forwarding of {@link android.view.View#setVisibility(int)}. */ 302 public void setVisibility(int visibility) { 303 mView.setVisibility(visibility); 304 } 305 306 /** 307 * Notifies the {@link com.android.camera.filmstrip.DataAdapter} to 308 * resize the view. 309 */ 310 public void resizeView(Context context, int w, int h) { 311 mDataAdapter.resizeView(context, mDataId, mView, w, h); 312 } 313 314 /** 315 * Adds the view of the data to the view hierarchy if necessary. 316 */ 317 public void addViewToHierarchy() { 318 if (indexOfChild(mView) < 0) { 319 mData.prepare(); 320 addView(mView); 321 } else { 322 setVisibility(View.VISIBLE); 323 setAlpha(1f); 324 setTranslationX(0); 325 setTranslationY(0); 326 } 327 } 328 329 /** 330 * Removes from the hierarchy. Keeps the view in the view hierarchy if 331 * view type is {@code VIEW_TYPE_STICKY} and set to invisible instead. 332 * 333 * @param force {@code true} to remove the view from the hierarchy 334 * regardless of the view type. 335 */ 336 public void removeViewFromHierarchy(boolean force) { 337 if (force || mData.getViewType() != ImageData.VIEW_TYPE_STICKY) { 338 removeView(mView); 339 mData.recycle(); 340 } else { 341 setVisibility(View.INVISIBLE); 342 } 343 } 344 345 /** 346 * Brings the view to front by 347 * {@link #bringChildToFront(android.view.View)} 348 */ 349 public void bringViewToFront() { 350 bringChildToFront(mView); 351 } 352 353 /** 354 * The visual x position of this view, in pixels. 355 */ 356 public float getX() { 357 return mView.getX(); 358 } 359 360 /** 361 * The visual y position of this view, in pixels. 362 */ 363 public float getY() { 364 return mView.getY(); 365 } 366 367 /** 368 * Forwarding of {@link android.view.View#measure(int, int)}. 369 */ 370 public void measure(int widthSpec, int heightSpec) { 371 mView.measure(widthSpec, heightSpec); 372 } 373 374 private void layoutAt(int left, int top) { 375 mView.layout(left, top, left + mView.getMeasuredWidth(), 376 top + mView.getMeasuredHeight()); 377 } 378 379 /** 380 * The bounding rect of the view. 381 */ 382 public RectF getViewRect() { 383 RectF r = new RectF(); 384 r.left = mView.getX(); 385 r.top = mView.getY(); 386 r.right = r.left + mView.getWidth() * mView.getScaleX(); 387 r.bottom = r.top + mView.getHeight() * mView.getScaleY(); 388 return r; 389 } 390 391 /** 392 * Layouts the view in the area assuming the center of the area is at a 393 * specific point of the whole filmstrip. 394 * 395 * @param drawArea The area when filmstrip will show in. 396 * @param refCenter The absolute X coordination in the whole filmstrip 397 * of the center of {@code drawArea}. 398 * @param scale The scale of the view on the filmstrip. 399 */ 400 public void layoutWithTranslationX(Rect drawArea, int refCenter, float scale) { 401 final float translationX = 402 ((mTranslationXAnimator != null && mTranslationXAnimator.isRunning()) ? 403 (Float) mTranslationXAnimator.getAnimatedValue() : 0); 404 int left = 405 (int) (drawArea.centerX() + (mLeftPosition - refCenter + translationX) * scale); 406 int top = (int) (drawArea.centerY() - (mView.getMeasuredHeight() / 2) * scale); 407 layoutAt(left, top); 408 mView.setScaleX(scale); 409 mView.setScaleY(scale); 410 411 // update mViewArea for touch detection. 412 int l = mView.getLeft(); 413 int t = mView.getTop(); 414 mViewArea.set(l, t, 415 l + mView.getMeasuredWidth() * scale, 416 t + mView.getMeasuredHeight() * scale); 417 } 418 419 /** Returns true if the point is in the view. */ 420 public boolean areaContains(float x, float y) { 421 return mViewArea.contains(x, y); 422 } 423 424 /** 425 * Return the width of the view. 426 */ 427 public int getWidth() { 428 return mView.getWidth(); 429 } 430 431 public void copyAttributes(ViewItem item) { 432 setLeftPosition(item.getLeftPosition()); 433 // X 434 setTranslationX(item.getTranslationX()); 435 if (item.mTranslationXAnimator != null) { 436 mTranslationXAnimator = item.mTranslationXAnimator; 437 mTranslationXAnimator.removeAllUpdateListeners(); 438 mTranslationXAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 439 @Override 440 public void onAnimationUpdate(ValueAnimator valueAnimator) { 441 // We invalidate the filmstrip view instead of setting the 442 // translation X because the translation X of the view is 443 // touched in onLayout(). See the documentation of 444 // animateTranslationX(). 445 invalidate(); 446 } 447 }); 448 } 449 // Y 450 setTranslationY(item.getTranslationY()); 451 if (item.mTranslationYAnimator != null) { 452 mTranslationYAnimator = item.mTranslationYAnimator; 453 mTranslationYAnimator.removeAllUpdateListeners(); 454 mTranslationYAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 455 @Override 456 public void onAnimationUpdate(ValueAnimator valueAnimator) { 457 setTranslationY((Float) valueAnimator.getAnimatedValue()); 458 } 459 }); 460 } 461 // Alpha 462 setAlpha(item.getAlpha()); 463 if (item.mAlphaAnimator != null) { 464 mAlphaAnimator = item.mAlphaAnimator; 465 mAlphaAnimator.removeAllUpdateListeners(); 466 mAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 467 @Override 468 public void onAnimationUpdate(ValueAnimator valueAnimator) { 469 ViewItem.this.setAlpha((Float) valueAnimator.getAnimatedValue()); 470 } 471 }); 472 } 473 } 474 475 /** 476 * Apply a scale factor (i.e. {@code postScale}) on top of current scale at 477 * pivot point ({@code focusX}, {@code focusY}). Visually it should be the 478 * same as post concatenating current view's matrix with specified scale. 479 */ 480 void postScale(float focusX, float focusY, float postScale, int viewportWidth, 481 int viewportHeight) { 482 float transX = mView.getTranslationX(); 483 float transY = mView.getTranslationY(); 484 // Pivot point is top left of the view, so we need to translate 485 // to scale around focus point 486 transX -= (focusX - getX()) * (postScale - 1f); 487 transY -= (focusY - getY()) * (postScale - 1f); 488 float scaleX = mView.getScaleX() * postScale; 489 float scaleY = mView.getScaleY() * postScale; 490 updateTransform(transX, transY, scaleX, scaleY, viewportWidth, 491 viewportHeight); 492 } 493 494 void updateTransform(float transX, float transY, float scaleX, float scaleY, 495 int viewportWidth, int viewportHeight) { 496 float left = transX + mView.getLeft(); 497 float top = transY + mView.getTop(); 498 RectF r = ZoomView.adjustToFitInBounds(new RectF(left, top, 499 left + mView.getWidth() * scaleX, 500 top + mView.getHeight() * scaleY), 501 viewportWidth, viewportHeight); 502 mView.setScaleX(scaleX); 503 mView.setScaleY(scaleY); 504 transX = r.left - mView.getLeft(); 505 transY = r.top - mView.getTop(); 506 mView.setTranslationX(transX); 507 mView.setTranslationY(transY); 508 } 509 510 void resetTransform() { 511 mView.setScaleX(FULL_SCREEN_SCALE); 512 mView.setScaleY(FULL_SCREEN_SCALE); 513 mView.setTranslationX(0f); 514 mView.setTranslationY(0f); 515 } 516 517 @Override 518 public String toString() { 519 return "DataID = " + mDataId + "\n\t left = " + mLeftPosition 520 + "\n\t viewArea = " + mViewArea 521 + "\n\t centerX = " + getCenterX() 522 + "\n\t view MeasuredSize = " 523 + mView.getMeasuredWidth() + ',' + mView.getMeasuredHeight() 524 + "\n\t view Size = " + mView.getWidth() + ',' + mView.getHeight() 525 + "\n\t view scale = " + mView.getScaleX(); 526 } 527 } 528 529 /** Constructor. */ 530 public FilmstripView(Context context) { 531 super(context); 532 init((CameraActivity) context); 533 } 534 535 /** Constructor. */ 536 public FilmstripView(Context context, AttributeSet attrs) { 537 super(context, attrs); 538 init((CameraActivity) context); 539 } 540 541 /** Constructor. */ 542 public FilmstripView(Context context, AttributeSet attrs, int defStyle) { 543 super(context, attrs, defStyle); 544 init((CameraActivity) context); 545 } 546 547 private void init(CameraActivity cameraActivity) { 548 setWillNotDraw(false); 549 mActivity = cameraActivity; 550 mScale = 1.0f; 551 mDataIdOnUserScrolling = 0; 552 mController = new MyController(cameraActivity); 553 mViewAnimInterpolator = new DecelerateInterpolator(); 554 mZoomView = new ZoomView(cameraActivity); 555 mZoomView.setVisibility(GONE); 556 addView(mZoomView); 557 558 mGestureListener = new MyGestureReceiver(); 559 mGestureRecognizer = 560 new FilmstripGestureRecognizer(cameraActivity, mGestureListener); 561 mSlop = (int) getContext().getResources().getDimension(R.dimen.pie_touch_slop); 562 DisplayMetrics metrics = new DisplayMetrics(); 563 mActivity.getWindowManager().getDefaultDisplay().getMetrics(metrics); 564 // Allow over scaling because on high density screens, pixels are too 565 // tiny to clearly see the details at 1:1 zoom. We should not scale 566 // beyond what 1:1 would look like on a medium density screen, as 567 // scaling beyond that would only yield blur. 568 mOverScaleFactor = (float) metrics.densityDpi / (float) DisplayMetrics.DENSITY_HIGH; 569 if (mOverScaleFactor < 1f) { 570 mOverScaleFactor = 1f; 571 } 572 } 573 574 /** 575 * Returns the controller. 576 * 577 * @return The {@code Controller}. 578 */ 579 public FilmstripController getController() { 580 return mController; 581 } 582 583 private void setListener(FilmstripController.FilmstripListener l) { 584 mListener = l; 585 } 586 587 private void setViewGap(int viewGap) { 588 mViewGapInPixel = viewGap; 589 } 590 591 /** 592 * Checks if the data is at the center. 593 * 594 * @param id The id of the data to check. 595 * @return {@code True} if the data is currently at the center. 596 */ 597 private boolean isDataAtCenter(int id) { 598 if (mViewItem[mCurrentItem] == null) { 599 return false; 600 } 601 if (mViewItem[mCurrentItem].getId() == id 602 && isCurrentItemCentered()) { 603 return true; 604 } 605 return false; 606 } 607 608 /** Returns [width, height] preserving image aspect ratio. */ 609 private int[] calculateChildDimension( 610 int imageWidth, int imageHeight, int imageOrientation, 611 int boundWidth, int boundHeight) { 612 if (imageOrientation == 90 || imageOrientation == 270) { 613 // Swap width and height. 614 int savedWidth = imageWidth; 615 imageWidth = imageHeight; 616 imageHeight = savedWidth; 617 } 618 if (imageWidth == ImageData.SIZE_FULL 619 || imageHeight == ImageData.SIZE_FULL) { 620 imageWidth = boundWidth; 621 imageHeight = boundHeight; 622 } 623 624 int[] ret = new int[2]; 625 ret[0] = boundWidth; 626 ret[1] = boundHeight; 627 628 if (imageWidth * ret[1] > ret[0] * imageHeight) { 629 ret[1] = imageHeight * ret[0] / imageWidth; 630 } else { 631 ret[0] = imageWidth * ret[1] / imageHeight; 632 } 633 634 return ret; 635 } 636 637 private void measureViewItem(ViewItem item, int boundWidth, int boundHeight) { 638 int id = item.getId(); 639 ImageData imageData = mDataAdapter.getImageData(id); 640 if (imageData == null) { 641 Log.e(TAG, "trying to measure a null item"); 642 return; 643 } 644 645 int[] dim = calculateChildDimension(imageData.getWidth(), 646 imageData.getHeight(), 647 imageData.getOrientation(), boundWidth, boundHeight); 648 649 item.measure(MeasureSpec.makeMeasureSpec(dim[0], MeasureSpec.EXACTLY), 650 MeasureSpec.makeMeasureSpec(dim[1], MeasureSpec.EXACTLY)); 651 } 652 653 @Override 654 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 655 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 656 657 int boundWidth = MeasureSpec.getSize(widthMeasureSpec); 658 int boundHeight = MeasureSpec.getSize(heightMeasureSpec); 659 if (boundWidth == 0 || boundHeight == 0) { 660 // Either width or height is unknown, can't measure children yet. 661 return; 662 } 663 664 for (ViewItem item : mViewItem) { 665 if (item != null) { 666 measureViewItem(item, boundWidth, boundHeight); 667 } 668 } 669 clampCenterX(); 670 // Measure zoom view 671 mZoomView.measure(MeasureSpec.makeMeasureSpec(widthMeasureSpec, MeasureSpec.EXACTLY), 672 MeasureSpec.makeMeasureSpec(heightMeasureSpec, MeasureSpec.EXACTLY)); 673 } 674 675 private int findTheNearestView(int pointX) { 676 677 int nearest = 0; 678 // Find the first non-null ViewItem. 679 while (nearest < BUFFER_SIZE 680 && (mViewItem[nearest] == null || mViewItem[nearest].getLeftPosition() == -1)) { 681 nearest++; 682 } 683 // No existing available ViewItem 684 if (nearest == BUFFER_SIZE) { 685 return -1; 686 } 687 688 int min = Math.abs(pointX - mViewItem[nearest].getCenterX()); 689 690 for (int itemID = nearest + 1; itemID < BUFFER_SIZE && mViewItem[itemID] != null; itemID++) { 691 // Not measured yet. 692 if (mViewItem[itemID].getLeftPosition() == -1) 693 continue; 694 695 int c = mViewItem[itemID].getCenterX(); 696 int dist = Math.abs(pointX - c); 697 if (dist < min) { 698 min = dist; 699 nearest = itemID; 700 } 701 } 702 return nearest; 703 } 704 705 private ViewItem buildItemFromData(int dataID) { 706 ImageData data = mDataAdapter.getImageData(dataID); 707 if (data == null) { 708 return null; 709 } 710 711 int maxEdge = (int) ((float) Math.max(this.getHeight(), this.getWidth()) 712 * FILM_STRIP_SCALE); 713 mDataAdapter.suggestViewSizeBound(maxEdge, maxEdge); 714 data.prepare(); 715 View v = mDataAdapter.getView(mActivity, dataID); 716 if (v == null) { 717 return null; 718 } 719 ViewItem item = new ViewItem(dataID, v, data); 720 item.addViewToHierarchy(); 721 return item; 722 } 723 724 private void checkItemAtMaxSize() { 725 ViewItem item = mViewItem[mCurrentItem]; 726 if (item.isMaximumBitmapRequested()) { 727 return; 728 }; 729 item.setMaximumBitmapRequested(); 730 // Request full size bitmap, or max that DataAdapter will create. 731 int id = item.getId(); 732 int h = mDataAdapter.getImageData(id).getHeight(); 733 int w = mDataAdapter.getImageData(id).getWidth(); 734 item.resizeView(mActivity, w, h); 735 } 736 737 private void removeItem(int itemID) { 738 if (itemID >= mViewItem.length || mViewItem[itemID] == null) { 739 return; 740 } 741 ImageData data = mDataAdapter.getImageData(mViewItem[itemID].getId()); 742 if (data == null) { 743 Log.e(TAG, "trying to remove a null item"); 744 return; 745 } 746 mViewItem[itemID].removeViewFromHierarchy(false); 747 mViewItem[itemID] = null; 748 } 749 750 /** 751 * We try to keep the one closest to the center of the screen at position 752 * mCurrentItem. 753 */ 754 private void stepIfNeeded() { 755 if (!inFilmstrip() && !inFullScreen()) { 756 // The good timing to step to the next view is when everything is 757 // not in transition. 758 return; 759 } 760 final int nearest = findTheNearestView(mCenterX); 761 // no change made. 762 if (nearest == -1 || nearest == mCurrentItem) { 763 return; 764 } 765 766 int prevDataId = (mViewItem[mCurrentItem] == null ? -1 : mViewItem[mCurrentItem].getId()); 767 final int adjust = nearest - mCurrentItem; 768 if (adjust > 0) { 769 for (int k = 0; k < adjust; k++) { 770 removeItem(k); 771 } 772 for (int k = 0; k + adjust < BUFFER_SIZE; k++) { 773 mViewItem[k] = mViewItem[k + adjust]; 774 } 775 for (int k = BUFFER_SIZE - adjust; k < BUFFER_SIZE; k++) { 776 mViewItem[k] = null; 777 if (mViewItem[k - 1] != null) { 778 mViewItem[k] = buildItemFromData(mViewItem[k - 1].getId() + 1); 779 } 780 } 781 adjustChildZOrder(); 782 } else { 783 for (int k = BUFFER_SIZE - 1; k >= BUFFER_SIZE + adjust; k--) { 784 removeItem(k); 785 } 786 for (int k = BUFFER_SIZE - 1; k + adjust >= 0; k--) { 787 mViewItem[k] = mViewItem[k + adjust]; 788 } 789 for (int k = -1 - adjust; k >= 0; k--) { 790 mViewItem[k] = null; 791 if (mViewItem[k + 1] != null) { 792 mViewItem[k] = buildItemFromData(mViewItem[k + 1].getId() - 1); 793 } 794 } 795 } 796 invalidate(); 797 if (mListener != null) { 798 mListener.onDataFocusChanged(prevDataId, mViewItem[mCurrentItem].getId()); 799 } 800 } 801 802 /** 803 * Check the bounds of {@code mCenterX}. Always call this function after: 1. 804 * Any changes to {@code mCenterX}. 2. Any size change of the view items. 805 * 806 * @return Whether clamp happened. 807 */ 808 private boolean clampCenterX() { 809 ViewItem curr = mViewItem[mCurrentItem]; 810 if (curr == null) { 811 return false; 812 } 813 814 boolean stopScroll = false; 815 if (curr.getId() == 1 && mCenterX < curr.getCenterX() && mDataIdOnUserScrolling > 1 && 816 mDataAdapter.getImageData(0).getViewType() == ImageData.VIEW_TYPE_STICKY && 817 mController.isScrolling()) { 818 stopScroll = true; 819 } else { 820 if (curr.getId() == 0 && mCenterX < curr.getCenterX()) { 821 // Stop at the first ViewItem. 822 stopScroll = true; 823 } 824 } 825 if (curr.getId() == mDataAdapter.getTotalNumber() - 1 826 && mCenterX > curr.getCenterX()) { 827 // Stop at the end. 828 stopScroll = true; 829 } 830 831 if (stopScroll) { 832 mCenterX = curr.getCenterX(); 833 } 834 835 return stopScroll; 836 } 837 838 /** 839 * Reorders the child views to be consistent with their data ID. This method 840 * should be called after adding/removing views. 841 */ 842 private void adjustChildZOrder() { 843 for (int i = BUFFER_SIZE - 1; i >= 0; i--) { 844 if (mViewItem[i] == null) 845 continue; 846 mViewItem[i].bringViewToFront(); 847 } 848 // ZoomView is a special case to always be in the front. 849 bringChildToFront(mZoomView); 850 } 851 852 /** 853 * Returns the ID of the current item, or -1 if there is no data. 854 */ 855 private int getCurrentId() { 856 ViewItem current = mViewItem[mCurrentItem]; 857 if (current == null) { 858 return -1; 859 } 860 return current.getId(); 861 } 862 863 /** 864 * Keep the current item in the center. This functions does not check if the 865 * current item is null. 866 */ 867 private void snapInCenter() { 868 final ViewItem currItem = mViewItem[mCurrentItem]; 869 if (currItem == null) { 870 return; 871 } 872 final int currentViewCenter = currItem.getCenterX(); 873 if (mController.isScrolling() || mIsUserScrolling 874 || isCurrentItemCentered()) { 875 return; 876 } 877 878 int snapInTime = (int) (SNAP_IN_CENTER_TIME_MS 879 * ((float) Math.abs(mCenterX - currentViewCenter)) 880 / mDrawArea.width()); 881 mController.scrollToPosition(currentViewCenter, 882 snapInTime, false); 883 if (isViewTypeSticky(currItem) && !mController.isScaling() && mScale != FULL_SCREEN_SCALE) { 884 // Now going to full screen camera 885 mController.goToFullScreen(); 886 } 887 } 888 889 /** 890 * Translates the {@link ViewItem} on the left of the current one to match 891 * the full-screen layout. In full-screen, we show only one {@link ViewItem} 892 * which occupies the whole screen. The other left ones are put on the left 893 * side in full scales. Does nothing if there's no next item. 894 * 895 * @param currItem The item ID of the current one to be translated. 896 * @param drawAreaWidth The width of the current draw area. 897 * @param scaleFraction A {@code float} between 0 and 1. 0 if the current 898 * scale is {@code FILM_STRIP_SCALE}. 1 if the current scale is 899 * {@code FULL_SCREEN_SCALE}. 900 */ 901 private void translateLeftViewItem( 902 int currItem, int drawAreaWidth, float scaleFraction) { 903 if (currItem < 0 || currItem > BUFFER_SIZE - 1) { 904 Log.e(TAG, "currItem id out of bound."); 905 return; 906 } 907 908 final ViewItem curr = mViewItem[currItem]; 909 final ViewItem next = mViewItem[currItem + 1]; 910 if (curr == null || next == null) { 911 Log.e(TAG, "Invalid view item (curr or next == null). curr = " 912 + currItem); 913 return; 914 } 915 916 final int currCenterX = curr.getCenterX(); 917 final int nextCenterX = next.getCenterX(); 918 final int translate = (int) ((nextCenterX - drawAreaWidth 919 - currCenterX) * scaleFraction); 920 921 curr.layoutWithTranslationX(mDrawArea, mCenterX, mScale); 922 curr.setAlpha(1f); 923 924 if (inFullScreen()) { 925 curr.setTranslationX(translate * (mCenterX - currCenterX) / (nextCenterX - currCenterX)); 926 } else { 927 curr.setTranslationX(translate); 928 } 929 } 930 931 /** 932 * Fade out the {@link ViewItem} on the right of the current one in 933 * full-screen layout. Does nothing if there's no previous item. 934 * 935 * @param currItemId The ID of the item to fade. 936 */ 937 private void fadeAndScaleRightViewItem(int currItemId) { 938 if (currItemId < 1 || currItemId > BUFFER_SIZE) { 939 Log.e(TAG, "currItem id out of bound."); 940 return; 941 } 942 943 final ViewItem currItem = mViewItem[currItemId]; 944 final ViewItem prevItem = mViewItem[currItemId - 1]; 945 if (currItem == null || prevItem == null) { 946 Log.e(TAG, "Invalid view item (curr or prev == null). curr = " 947 + currItemId); 948 return; 949 } 950 951 if (currItemId > mCurrentItem + 1) { 952 // Every item not right next to the mCurrentItem is invisible. 953 currItem.setVisibility(INVISIBLE); 954 return; 955 } 956 final int prevCenterX = prevItem.getCenterX(); 957 if (mCenterX <= prevCenterX) { 958 // Shortcut. If the position is at the center of the previous one, 959 // set to invisible too. 960 currItem.setVisibility(INVISIBLE); 961 return; 962 } 963 final int currCenterX = currItem.getCenterX(); 964 final float fadeDownFraction = 965 ((float) mCenterX - prevCenterX) / (currCenterX - prevCenterX); 966 currItem.layoutWithTranslationX(mDrawArea, currCenterX, 967 FILM_STRIP_SCALE + (1f - FILM_STRIP_SCALE) * fadeDownFraction); 968 currItem.setAlpha(fadeDownFraction); 969 currItem.setTranslationX(0); 970 currItem.setVisibility(VISIBLE); 971 } 972 973 private void layoutViewItems(boolean layoutChanged) { 974 if (mViewItem[mCurrentItem] == null || 975 mDrawArea.width() == 0 || 976 mDrawArea.height() == 0) { 977 return; 978 } 979 980 // If the layout changed, we need to adjust the current position so 981 // that if an item is centered before the change, it's still centered. 982 if (layoutChanged) { 983 mViewItem[mCurrentItem].setLeftPosition( 984 mCenterX - mViewItem[mCurrentItem].getMeasuredWidth() / 2); 985 } 986 987 if (mController.isZoomStarted()) { 988 return; 989 } 990 /** 991 * Transformed scale fraction between 0 and 1. 0 if the scale is 992 * {@link FILM_STRIP_SCALE}. 1 if the scale is {@link FULL_SCREEN_SCALE} 993 * . 994 */ 995 final float scaleFraction = mViewAnimInterpolator.getInterpolation( 996 (mScale - FILM_STRIP_SCALE) / (FULL_SCREEN_SCALE - FILM_STRIP_SCALE)); 997 final int fullScreenWidth = mDrawArea.width() + mViewGapInPixel; 998 999 // Decide the position for all view items on the left and the right 1000 // first. 1001 1002 // Left items. 1003 for (int itemID = mCurrentItem - 1; itemID >= 0; itemID--) { 1004 final ViewItem curr = mViewItem[itemID]; 1005 if (curr == null) { 1006 break; 1007 } 1008 1009 // First, layout relatively to the next one. 1010 final int currLeft = mViewItem[itemID + 1].getLeftPosition() 1011 - curr.getMeasuredWidth() - mViewGapInPixel; 1012 curr.setLeftPosition(currLeft); 1013 } 1014 // Right items. 1015 for (int itemID = mCurrentItem + 1; itemID < BUFFER_SIZE; itemID++) { 1016 final ViewItem curr = mViewItem[itemID]; 1017 if (curr == null) { 1018 break; 1019 } 1020 1021 // First, layout relatively to the previous one. 1022 final ViewItem prev = mViewItem[itemID - 1]; 1023 final int currLeft = 1024 prev.getLeftPosition() + prev.getMeasuredWidth() 1025 + mViewGapInPixel; 1026 curr.setLeftPosition(currLeft); 1027 } 1028 1029 // Special case for the one immediately on the right of the camera 1030 // preview. 1031 boolean immediateRight = 1032 (mViewItem[mCurrentItem].getId() == 1 && 1033 mDataAdapter.getImageData(0).getViewType() == ImageData.VIEW_TYPE_STICKY); 1034 1035 // Layout the current ViewItem first. 1036 if (immediateRight) { 1037 // Just do a simple layout without any special translation or 1038 // fading. The implementation in Gallery does not push the first 1039 // photo to the bottom of the camera preview. Simply place the 1040 // photo on the right of the preview. 1041 final ViewItem currItem = mViewItem[mCurrentItem]; 1042 currItem.layoutWithTranslationX(mDrawArea, mCenterX, mScale); 1043 currItem.setTranslationX(0f); 1044 currItem.setAlpha(1f); 1045 } else if (scaleFraction == 1f) { 1046 final ViewItem currItem = mViewItem[mCurrentItem]; 1047 final int currCenterX = currItem.getCenterX(); 1048 if (mCenterX < currCenterX) { 1049 // In full-screen and mCenterX is on the left of the center, 1050 // we draw the current one to "fade down". 1051 fadeAndScaleRightViewItem(mCurrentItem); 1052 } else if (mCenterX > currCenterX) { 1053 // In full-screen and mCenterX is on the right of the center, 1054 // we draw the current one translated. 1055 translateLeftViewItem(mCurrentItem, fullScreenWidth, scaleFraction); 1056 } else { 1057 currItem.layoutWithTranslationX(mDrawArea, mCenterX, mScale); 1058 currItem.setTranslationX(0f); 1059 currItem.setAlpha(1f); 1060 } 1061 } else { 1062 final ViewItem currItem = mViewItem[mCurrentItem]; 1063 // The normal filmstrip has no translation for the current item. If 1064 // it has translation before, gradually set it to zero. 1065 currItem.setTranslationX(currItem.getTranslationX() * scaleFraction); 1066 currItem.layoutWithTranslationX(mDrawArea, mCenterX, mScale); 1067 if (mViewItem[mCurrentItem - 1] == null) { 1068 currItem.setAlpha(1f); 1069 } else { 1070 final int currCenterX = currItem.getCenterX(); 1071 final int prevCenterX = mViewItem[mCurrentItem - 1].getCenterX(); 1072 final float fadeDownFraction = 1073 ((float) mCenterX - prevCenterX) / (currCenterX - prevCenterX); 1074 currItem.setAlpha( 1075 (1 - fadeDownFraction) * (1 - scaleFraction) + fadeDownFraction); 1076 } 1077 } 1078 1079 // Layout the rest dependent on the current scale. 1080 1081 // Items on the left 1082 for (int itemID = mCurrentItem - 1; itemID >= 0; itemID--) { 1083 final ViewItem curr = mViewItem[itemID]; 1084 if (curr == null) { 1085 break; 1086 } 1087 translateLeftViewItem(itemID, fullScreenWidth, scaleFraction); 1088 } 1089 1090 // Items on the right 1091 for (int itemID = mCurrentItem + 1; itemID < BUFFER_SIZE; itemID++) { 1092 final ViewItem curr = mViewItem[itemID]; 1093 if (curr == null) { 1094 break; 1095 } 1096 1097 curr.layoutWithTranslationX(mDrawArea, mCenterX, mScale); 1098 if (curr.getId() == 1 && isViewTypeSticky(curr)) { 1099 // Special case for the one next to the camera preview. 1100 curr.setAlpha(1f); 1101 continue; 1102 } 1103 1104 if (scaleFraction == 1) { 1105 // It's in full-screen mode. 1106 fadeAndScaleRightViewItem(itemID); 1107 } else { 1108 if (curr.getVisibility() == INVISIBLE) { 1109 curr.setVisibility(VISIBLE); 1110 } 1111 if (itemID == mCurrentItem + 1) { 1112 curr.setAlpha(1f - scaleFraction); 1113 } else { 1114 if (scaleFraction == 0f) { 1115 curr.setAlpha(1f); 1116 } else { 1117 curr.setVisibility(INVISIBLE); 1118 } 1119 } 1120 curr.setTranslationX( 1121 (mViewItem[mCurrentItem].getLeftPosition() - curr.getLeftPosition()) * 1122 scaleFraction); 1123 } 1124 } 1125 1126 stepIfNeeded(); 1127 } 1128 1129 private boolean isViewTypeSticky(ViewItem item) { 1130 if (item == null) { 1131 return false; 1132 } 1133 return mDataAdapter.getImageData(item.getId()).getViewType() == 1134 ImageData.VIEW_TYPE_STICKY; 1135 } 1136 1137 @Override 1138 public void onDraw(Canvas c) { 1139 // TODO: remove layoutViewItems() here. 1140 layoutViewItems(false); 1141 super.onDraw(c); 1142 } 1143 1144 @Override 1145 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1146 mDrawArea.left = l; 1147 mDrawArea.top = t; 1148 mDrawArea.right = r; 1149 mDrawArea.bottom = b; 1150 mZoomView.layout(mDrawArea.left, mDrawArea.top, mDrawArea.right, mDrawArea.bottom); 1151 // TODO: Need a more robust solution to decide when to re-layout 1152 // If in the middle of zooming, only re-layout when the layout has 1153 // changed. 1154 if (!mController.isZoomStarted() || changed) { 1155 resetZoomView(); 1156 layoutViewItems(changed); 1157 } 1158 } 1159 1160 /** 1161 * Clears the translation and scale that has been set on the view, cancels 1162 * any loading request for image partial decoding, and hides zoom view. This 1163 * is needed for when there is a layout change (e.g. when users re-enter the 1164 * app, or rotate the device, etc). 1165 */ 1166 private void resetZoomView() { 1167 if (!mController.isZoomStarted()) { 1168 return; 1169 } 1170 ViewItem current = mViewItem[mCurrentItem]; 1171 if (current == null) { 1172 return; 1173 } 1174 mScale = FULL_SCREEN_SCALE; 1175 mController.cancelZoomAnimation(); 1176 mController.cancelFlingAnimation(); 1177 current.resetTransform(); 1178 mController.cancelLoadingZoomedImage(); 1179 mZoomView.setVisibility(GONE); 1180 mController.setSurroundingViewsVisible(true); 1181 } 1182 1183 private void hideZoomView() { 1184 if (mController.isZoomStarted()) { 1185 mController.cancelLoadingZoomedImage(); 1186 mZoomView.setVisibility(GONE); 1187 } 1188 } 1189 1190 private void slideViewBack(ViewItem item) { 1191 item.animateTranslationX(0, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator); 1192 item.animateTranslationY(0, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator); 1193 item.animateAlpha(1f, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator); 1194 } 1195 1196 private void animateItemRemoval(int dataID, final ImageData data) { 1197 if (mScale > FULL_SCREEN_SCALE) { 1198 resetZoomView(); 1199 } 1200 int removedItemId = findItemByDataID(dataID); 1201 1202 // adjust the data id to be consistent 1203 for (int i = 0; i < BUFFER_SIZE; i++) { 1204 if (mViewItem[i] == null || mViewItem[i].getId() <= dataID) { 1205 continue; 1206 } 1207 mViewItem[i].setId(mViewItem[i].getId() - 1); 1208 } 1209 if (removedItemId == -1) { 1210 return; 1211 } 1212 1213 final ViewItem removedItem = mViewItem[removedItemId]; 1214 final int offsetX = removedItem.getMeasuredWidth() + mViewGapInPixel; 1215 1216 for (int i = removedItemId + 1; i < BUFFER_SIZE; i++) { 1217 if (mViewItem[i] != null) { 1218 mViewItem[i].setLeftPosition(mViewItem[i].getLeftPosition() - offsetX); 1219 } 1220 } 1221 1222 if (removedItemId >= mCurrentItem 1223 && mViewItem[removedItemId].getId() < mDataAdapter.getTotalNumber()) { 1224 // Fill the removed item by left shift when the current one or 1225 // anyone on the right is removed, and there's more data on the 1226 // right available. 1227 for (int i = removedItemId; i < BUFFER_SIZE - 1; i++) { 1228 mViewItem[i] = mViewItem[i + 1]; 1229 } 1230 1231 // pull data out from the DataAdapter for the last one. 1232 int curr = BUFFER_SIZE - 1; 1233 int prev = curr - 1; 1234 if (mViewItem[prev] != null) { 1235 mViewItem[curr] = buildItemFromData(mViewItem[prev].getId() + 1); 1236 } 1237 1238 // The animation part. 1239 if (inFullScreen()) { 1240 mViewItem[mCurrentItem].setVisibility(VISIBLE); 1241 ViewItem nextItem = mViewItem[mCurrentItem + 1]; 1242 if (nextItem != null) { 1243 nextItem.setVisibility(INVISIBLE); 1244 } 1245 } 1246 1247 // Translate the views to their original places. 1248 for (int i = removedItemId; i < BUFFER_SIZE; i++) { 1249 if (mViewItem[i] != null) { 1250 mViewItem[i].setTranslationX(offsetX); 1251 } 1252 } 1253 1254 // The end of the filmstrip might have been changed. 1255 // The mCenterX might be out of the bound. 1256 ViewItem currItem = mViewItem[mCurrentItem]; 1257 if (currItem.getId() == mDataAdapter.getTotalNumber() - 1 1258 && mCenterX > currItem.getCenterX()) { 1259 int adjustDiff = currItem.getCenterX() - mCenterX; 1260 mCenterX = currItem.getCenterX(); 1261 for (int i = 0; i < BUFFER_SIZE; i++) { 1262 if (mViewItem[i] != null) { 1263 mViewItem[i].translateXScaledBy(adjustDiff); 1264 } 1265 } 1266 } 1267 } else { 1268 // fill the removed place by right shift 1269 mCenterX -= offsetX; 1270 1271 for (int i = removedItemId; i > 0; i--) { 1272 mViewItem[i] = mViewItem[i - 1]; 1273 } 1274 1275 // pull data out from the DataAdapter for the first one. 1276 int curr = 0; 1277 int next = curr + 1; 1278 if (mViewItem[next] != null) { 1279 mViewItem[curr] = buildItemFromData(mViewItem[next].getId() - 1); 1280 } 1281 1282 // Translate the views to their original places. 1283 for (int i = removedItemId; i >= 0; i--) { 1284 if (mViewItem[i] != null) { 1285 mViewItem[i].setTranslationX(-offsetX); 1286 } 1287 } 1288 } 1289 1290 int transY = getHeight() / 8; 1291 if (removedItem.getTranslationY() < 0) { 1292 transY = -transY; 1293 } 1294 removedItem.animateTranslationY(removedItem.getTranslationY() + transY, 1295 GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator); 1296 removedItem.animateAlpha(0f, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator); 1297 postDelayed(new Runnable() { 1298 @Override 1299 public void run() { 1300 removedItem.removeViewFromHierarchy(false); 1301 } 1302 }, GEOMETRY_ADJUST_TIME_MS); 1303 1304 adjustChildZOrder(); 1305 invalidate(); 1306 1307 // Now, slide every one back. 1308 if (mViewItem[mCurrentItem] == null) { 1309 return; 1310 } 1311 for (int i = 0; i < BUFFER_SIZE; i++) { 1312 if (mViewItem[i] != null 1313 && mViewItem[i].getTranslationX() != 0f) { 1314 slideViewBack(mViewItem[i]); 1315 } 1316 } 1317 if (isCurrentItemCentered() && isViewTypeSticky(mViewItem[mCurrentItem])) { 1318 // Special case for scrolling onto the camera preview after removal. 1319 mController.goToFullScreen(); 1320 } 1321 } 1322 1323 // returns -1 on failure. 1324 private int findItemByDataID(int dataID) { 1325 for (int i = 0; i < BUFFER_SIZE; i++) { 1326 if (mViewItem[i] != null 1327 && mViewItem[i].getId() == dataID) { 1328 return i; 1329 } 1330 } 1331 return -1; 1332 } 1333 1334 private void updateInsertion(int dataID) { 1335 int insertedItemId = findItemByDataID(dataID); 1336 if (insertedItemId == -1) { 1337 // Not in the current item buffers. Check if it's inserted 1338 // at the end. 1339 if (dataID == mDataAdapter.getTotalNumber() - 1) { 1340 int prev = findItemByDataID(dataID - 1); 1341 if (prev >= 0 && prev < BUFFER_SIZE - 1) { 1342 // The previous data is in the buffer and we still 1343 // have room for the inserted data. 1344 insertedItemId = prev + 1; 1345 } 1346 } 1347 } 1348 1349 // adjust the data id to be consistent 1350 for (int i = 0; i < BUFFER_SIZE; i++) { 1351 if (mViewItem[i] == null || mViewItem[i].getId() < dataID) { 1352 continue; 1353 } 1354 mViewItem[i].setId(mViewItem[i].getId() + 1); 1355 } 1356 if (insertedItemId == -1) { 1357 return; 1358 } 1359 1360 final ImageData data = mDataAdapter.getImageData(dataID); 1361 int[] dim = calculateChildDimension( 1362 data.getWidth(), data.getHeight(), data.getOrientation(), 1363 getMeasuredWidth(), getMeasuredHeight()); 1364 final int offsetX = dim[0] + mViewGapInPixel; 1365 ViewItem viewItem = buildItemFromData(dataID); 1366 1367 if (insertedItemId >= mCurrentItem) { 1368 if (insertedItemId == mCurrentItem) { 1369 viewItem.setLeftPosition(mViewItem[mCurrentItem].getLeftPosition()); 1370 } 1371 // Shift right to make rooms for newly inserted item. 1372 removeItem(BUFFER_SIZE - 1); 1373 for (int i = BUFFER_SIZE - 1; i > insertedItemId; i--) { 1374 mViewItem[i] = mViewItem[i - 1]; 1375 if (mViewItem[i] != null) { 1376 mViewItem[i].setTranslationX(-offsetX); 1377 slideViewBack(mViewItem[i]); 1378 } 1379 } 1380 } else { 1381 // Shift left. Put the inserted data on the left instead of the 1382 // found position. 1383 --insertedItemId; 1384 if (insertedItemId < 0) { 1385 return; 1386 } 1387 removeItem(0); 1388 for (int i = 1; i <= insertedItemId; i++) { 1389 if (mViewItem[i] != null) { 1390 mViewItem[i].setTranslationX(offsetX); 1391 slideViewBack(mViewItem[i]); 1392 mViewItem[i - 1] = mViewItem[i]; 1393 } 1394 } 1395 } 1396 1397 mViewItem[insertedItemId] = viewItem; 1398 viewItem.setAlpha(0f); 1399 viewItem.setTranslationY(getHeight() / 8); 1400 slideViewBack(viewItem); 1401 adjustChildZOrder(); 1402 invalidate(); 1403 } 1404 1405 private void setDataAdapter(DataAdapter adapter) { 1406 mDataAdapter = adapter; 1407 mDataAdapter.suggestViewSizeBound(getMeasuredWidth(), getMeasuredHeight()); 1408 mDataAdapter.setListener(new DataAdapter.Listener() { 1409 @Override 1410 public void onDataLoaded() { 1411 reload(); 1412 } 1413 1414 @Override 1415 public void onDataUpdated(DataAdapter.UpdateReporter reporter) { 1416 update(reporter); 1417 } 1418 1419 @Override 1420 public void onDataInserted(int dataId, ImageData data) { 1421 if (mViewItem[mCurrentItem] == null) { 1422 // empty now, simply do a reload. 1423 reload(); 1424 } else { 1425 updateInsertion(dataId); 1426 } 1427 if (mListener != null) { 1428 mListener.onDataFocusChanged(dataId, getCurrentId()); 1429 } 1430 } 1431 1432 @Override 1433 public void onDataRemoved(int dataId, ImageData data) { 1434 animateItemRemoval(dataId, data); 1435 if (mListener != null) { 1436 mListener.onDataFocusChanged(dataId, getCurrentId()); 1437 } 1438 } 1439 }); 1440 } 1441 1442 private boolean inFilmstrip() { 1443 return (mScale == FILM_STRIP_SCALE); 1444 } 1445 1446 private boolean inFullScreen() { 1447 return (mScale == FULL_SCREEN_SCALE); 1448 } 1449 1450 private boolean isCameraPreview() { 1451 return isViewTypeSticky(mViewItem[mCurrentItem]); 1452 } 1453 1454 private boolean inCameraFullscreen() { 1455 return isDataAtCenter(0) && inFullScreen() 1456 && (isViewTypeSticky(mViewItem[mCurrentItem])); 1457 } 1458 1459 @Override 1460 public boolean onInterceptTouchEvent(MotionEvent ev) { 1461 if (!inFullScreen() || mController.isScrolling()) { 1462 return true; 1463 } 1464 1465 if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { 1466 mCheckToIntercept = true; 1467 mDown = MotionEvent.obtain(ev); 1468 ViewItem viewItem = mViewItem[mCurrentItem]; 1469 // Do not intercept touch if swipe is not enabled 1470 if (viewItem != null && !mDataAdapter.canSwipeInFullScreen(viewItem.getId())) { 1471 mCheckToIntercept = false; 1472 } 1473 return false; 1474 } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { 1475 // Do not intercept touch once child is in zoom mode 1476 mCheckToIntercept = false; 1477 return false; 1478 } else { 1479 if (!mCheckToIntercept) { 1480 return false; 1481 } 1482 if (ev.getEventTime() - ev.getDownTime() > SWIPE_TIME_OUT) { 1483 return false; 1484 } 1485 int deltaX = (int) (ev.getX() - mDown.getX()); 1486 int deltaY = (int) (ev.getY() - mDown.getY()); 1487 if (ev.getActionMasked() == MotionEvent.ACTION_MOVE 1488 && deltaX < mSlop * (-1)) { 1489 // intercept left swipe 1490 if (Math.abs(deltaX) >= Math.abs(deltaY) * 2) { 1491 UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA, 1492 UsageStatistics.ACTION_FILMSTRIP, null); 1493 return true; 1494 } 1495 } 1496 } 1497 return false; 1498 } 1499 1500 @Override 1501 public boolean onTouchEvent(MotionEvent ev) { 1502 return mGestureRecognizer.onTouchEvent(ev); 1503 } 1504 1505 FilmstripGestureRecognizer.Listener getGestureListener() { 1506 return mGestureListener; 1507 } 1508 1509 private void updateViewItem(int itemID) { 1510 ViewItem item = mViewItem[itemID]; 1511 if (item == null) { 1512 Log.e(TAG, "trying to update an null item"); 1513 return; 1514 } 1515 item.removeViewFromHierarchy(true); 1516 1517 ViewItem newItem = buildItemFromData(item.getId()); 1518 if (newItem == null) { 1519 Log.e(TAG, "new item is null"); 1520 // keep using the old data. 1521 item.addViewToHierarchy(); 1522 return; 1523 } 1524 newItem.copyAttributes(item); 1525 mViewItem[itemID] = newItem; 1526 1527 boolean stopScroll = clampCenterX(); 1528 if (stopScroll) { 1529 mController.stopScrolling(true); 1530 } 1531 adjustChildZOrder(); 1532 invalidate(); 1533 if (mListener != null) { 1534 mListener.onDataUpdated(newItem.getId()); 1535 } 1536 } 1537 1538 /** Some of the data is changed. */ 1539 private void update(DataAdapter.UpdateReporter reporter) { 1540 // No data yet. 1541 if (mViewItem[mCurrentItem] == null) { 1542 reload(); 1543 return; 1544 } 1545 1546 // Check the current one. 1547 ViewItem curr = mViewItem[mCurrentItem]; 1548 int dataId = curr.getId(); 1549 if (reporter.isDataRemoved(dataId)) { 1550 reload(); 1551 return; 1552 } 1553 if (reporter.isDataUpdated(dataId)) { 1554 updateViewItem(mCurrentItem); 1555 final ImageData data = mDataAdapter.getImageData(dataId); 1556 if (!mIsUserScrolling && !mController.isScrolling()) { 1557 // If there is no scrolling at all, adjust mCenterX to place 1558 // the current item at the center. 1559 int[] dim = calculateChildDimension( 1560 data.getWidth(), data.getHeight(), data.getOrientation(), 1561 getMeasuredWidth(), getMeasuredHeight()); 1562 mCenterX = curr.getLeftPosition() + dim[0] / 2; 1563 } 1564 } 1565 1566 // Check left 1567 for (int i = mCurrentItem - 1; i >= 0; i--) { 1568 curr = mViewItem[i]; 1569 if (curr != null) { 1570 dataId = curr.getId(); 1571 if (reporter.isDataRemoved(dataId) || reporter.isDataUpdated(dataId)) { 1572 updateViewItem(i); 1573 } 1574 } else { 1575 ViewItem next = mViewItem[i + 1]; 1576 if (next != null) { 1577 mViewItem[i] = buildItemFromData(next.getId() - 1); 1578 } 1579 } 1580 } 1581 1582 // Check right 1583 for (int i = mCurrentItem + 1; i < BUFFER_SIZE; i++) { 1584 curr = mViewItem[i]; 1585 if (curr != null) { 1586 dataId = curr.getId(); 1587 if (reporter.isDataRemoved(dataId) || reporter.isDataUpdated(dataId)) { 1588 updateViewItem(i); 1589 } 1590 } else { 1591 ViewItem prev = mViewItem[i - 1]; 1592 if (prev != null) { 1593 mViewItem[i] = buildItemFromData(prev.getId() + 1); 1594 } 1595 } 1596 } 1597 adjustChildZOrder(); 1598 // Request a layout to find the measured width/height of the view first. 1599 requestLayout(); 1600 // Update photo sphere visibility after metadata fully written. 1601 } 1602 1603 /** 1604 * The whole data might be totally different. Flush all and load from the 1605 * start. Filmstrip will be centered on the first item, i.e. the camera 1606 * preview. 1607 */ 1608 private void reload() { 1609 mController.stopScrolling(true); 1610 mController.stopScale(); 1611 mDataIdOnUserScrolling = 0; 1612 1613 int prevId = -1; 1614 if (mViewItem[mCurrentItem] != null) { 1615 prevId = mViewItem[mCurrentItem].getId(); 1616 } 1617 1618 // Remove all views from the mViewItem buffer, except the camera view. 1619 for (int i = 0; i < mViewItem.length; i++) { 1620 if (mViewItem[i] == null) { 1621 continue; 1622 } 1623 mViewItem[i].removeViewFromHierarchy(false); 1624 } 1625 1626 // Clear out the mViewItems and rebuild with camera in the center. 1627 Arrays.fill(mViewItem, null); 1628 int dataNumber = mDataAdapter.getTotalNumber(); 1629 if (dataNumber == 0) { 1630 return; 1631 } 1632 1633 mViewItem[mCurrentItem] = buildItemFromData(0); 1634 if (mViewItem[mCurrentItem] == null) { 1635 return; 1636 } 1637 mViewItem[mCurrentItem].setLeftPosition(0); 1638 for (int i = mCurrentItem + 1; i < BUFFER_SIZE; i++) { 1639 mViewItem[i] = buildItemFromData(mViewItem[i - 1].getId() + 1); 1640 if (mViewItem[i] == null) { 1641 break; 1642 } 1643 } 1644 1645 // Ensure that the views in mViewItem will layout the first in the 1646 // center of the display upon a reload. 1647 mCenterX = -1; 1648 mScale = FILM_STRIP_SCALE; 1649 1650 adjustChildZOrder(); 1651 invalidate(); 1652 1653 if (mListener != null) { 1654 mListener.onDataReloaded(); 1655 mListener.onDataFocusChanged(prevId, mViewItem[mCurrentItem].getId()); 1656 } 1657 } 1658 1659 private void promoteData(int itemID, int dataID) { 1660 if (mListener != null) { 1661 mListener.onDataPromoted(dataID); 1662 } 1663 } 1664 1665 private void demoteData(int itemID, int dataID) { 1666 if (mListener != null) { 1667 mListener.onDataDemoted(dataID); 1668 } 1669 } 1670 1671 private void onEnterFilmstrip() { 1672 if (mListener != null) { 1673 mListener.onEnterFilmstrip(getCurrentId()); 1674 } 1675 } 1676 1677 private void onLeaveFilmstrip() { 1678 if (mListener != null) { 1679 mListener.onLeaveFilmstrip(getCurrentId()); 1680 } 1681 } 1682 1683 private void onEnterFullScreen() { 1684 if (mListener != null) { 1685 mListener.onEnterFullScreen(getCurrentId()); 1686 } 1687 } 1688 1689 private void onLeaveFullScreen() { 1690 if (mListener != null) { 1691 mListener.onLeaveFullScreen(getCurrentId()); 1692 } 1693 } 1694 1695 private void onEnterZoomView() { 1696 if (mListener != null) { 1697 mListener.onEnterZoomView(getCurrentId()); 1698 } 1699 } 1700 1701 /** 1702 * MyController controls all the geometry animations. It passively tells the 1703 * geometry information on demand. 1704 */ 1705 private class MyController implements FilmstripController { 1706 1707 private final ValueAnimator mScaleAnimator; 1708 private ValueAnimator mZoomAnimator; 1709 private AnimatorSet mFlingAnimator; 1710 1711 private final MyScroller mScroller; 1712 private boolean mCanStopScroll; 1713 1714 private final MyScroller.Listener mScrollerListener = 1715 new MyScroller.Listener() { 1716 @Override 1717 public void onScrollUpdate(int currX, int currY) { 1718 mCenterX = currX; 1719 1720 boolean stopScroll = clampCenterX(); 1721 if (stopScroll) { 1722 mController.stopScrolling(true); 1723 } 1724 invalidate(); 1725 } 1726 1727 @Override 1728 public void onScrollEnd() { 1729 mCanStopScroll = true; 1730 if (mViewItem[mCurrentItem] == null) { 1731 return; 1732 } 1733 snapInCenter(); 1734 if (isCurrentItemCentered() 1735 && isViewTypeSticky(mViewItem[mCurrentItem])) { 1736 // Special case for the scrolling end on the camera 1737 // preview. 1738 goToFullScreen(); 1739 } 1740 } 1741 }; 1742 1743 private final ValueAnimator.AnimatorUpdateListener mScaleAnimatorUpdateListener = 1744 new ValueAnimator.AnimatorUpdateListener() { 1745 @Override 1746 public void onAnimationUpdate(ValueAnimator animation) { 1747 if (mViewItem[mCurrentItem] == null) { 1748 return; 1749 } 1750 mScale = (Float) animation.getAnimatedValue(); 1751 invalidate(); 1752 } 1753 }; 1754 1755 MyController(Context context) { 1756 TimeInterpolator decelerateInterpolator = new DecelerateInterpolator(1.5f); 1757 mScroller = new MyScroller(mActivity, 1758 new Handler(mActivity.getMainLooper()), 1759 mScrollerListener, decelerateInterpolator); 1760 mCanStopScroll = true; 1761 1762 mScaleAnimator = new ValueAnimator(); 1763 mScaleAnimator.addUpdateListener(mScaleAnimatorUpdateListener); 1764 mScaleAnimator.setInterpolator(decelerateInterpolator); 1765 mScaleAnimator.addListener(new Animator.AnimatorListener() { 1766 @Override 1767 public void onAnimationStart(Animator animator) { 1768 if (mScale == FULL_SCREEN_SCALE) { 1769 onLeaveFullScreen(); 1770 } else { 1771 if (mScale == FILM_STRIP_SCALE) { 1772 onLeaveFilmstrip(); 1773 } 1774 } 1775 } 1776 1777 @Override 1778 public void onAnimationEnd(Animator animator) { 1779 if (mScale == FULL_SCREEN_SCALE) { 1780 onEnterFullScreen(); 1781 } else { 1782 if (mScale == FILM_STRIP_SCALE) { 1783 onEnterFilmstrip(); 1784 } 1785 } 1786 } 1787 1788 @Override 1789 public void onAnimationCancel(Animator animator) { 1790 1791 } 1792 1793 @Override 1794 public void onAnimationRepeat(Animator animator) { 1795 1796 } 1797 }); 1798 } 1799 1800 @Override 1801 public void setImageGap(int imageGap) { 1802 FilmstripView.this.setViewGap(imageGap); 1803 } 1804 1805 @Override 1806 public int getCurrentId() { 1807 return FilmstripView.this.getCurrentId(); 1808 } 1809 1810 @Override 1811 public void setDataAdapter(DataAdapter adapter) { 1812 FilmstripView.this.setDataAdapter(adapter); 1813 } 1814 1815 @Override 1816 public boolean inFilmstrip() { 1817 return FilmstripView.this.inFilmstrip(); 1818 } 1819 1820 @Override 1821 public boolean inFullScreen() { 1822 return FilmstripView.this.inFullScreen(); 1823 } 1824 1825 @Override 1826 public boolean isCameraPreview() { 1827 return FilmstripView.this.isCameraPreview(); 1828 } 1829 1830 @Override 1831 public boolean inCameraFullscreen() { 1832 return FilmstripView.this.inCameraFullscreen(); 1833 } 1834 1835 @Override 1836 public void setListener(FilmstripListener l) { 1837 FilmstripView.this.setListener(l); 1838 } 1839 1840 @Override 1841 public boolean isScrolling() { 1842 return !mScroller.isFinished(); 1843 } 1844 1845 @Override 1846 public boolean isScaling() { 1847 return mScaleAnimator.isRunning(); 1848 } 1849 1850 private int estimateMinX(int dataID, int leftPos, int viewWidth) { 1851 return leftPos - (dataID + 100) * (viewWidth + mViewGapInPixel); 1852 } 1853 1854 private int estimateMaxX(int dataID, int leftPos, int viewWidth) { 1855 return leftPos 1856 + (mDataAdapter.getTotalNumber() - dataID + 100) 1857 * (viewWidth + mViewGapInPixel); 1858 } 1859 1860 /** Zoom all the way in or out on the image at the given pivot point. */ 1861 private void zoomAt(final ViewItem current, final float focusX, final float focusY) { 1862 // End previous zoom animation, if any 1863 if (mZoomAnimator != null) { 1864 mZoomAnimator.end(); 1865 } 1866 // Calculate end scale 1867 final float maxScale = getCurrentDataMaxScale(false); 1868 final float endScale = mScale < maxScale - maxScale * TOLERANCE 1869 ? maxScale : FULL_SCREEN_SCALE; 1870 1871 mZoomAnimator = new ValueAnimator(); 1872 mZoomAnimator.setFloatValues(mScale, endScale); 1873 mZoomAnimator.setDuration(ZOOM_ANIMATION_DURATION_MS); 1874 mZoomAnimator.addListener(new Animator.AnimatorListener() { 1875 @Override 1876 public void onAnimationStart(Animator animation) { 1877 if (mScale == FULL_SCREEN_SCALE) { 1878 onEnterFullScreen(); 1879 setSurroundingViewsVisible(false); 1880 } 1881 cancelLoadingZoomedImage(); 1882 } 1883 1884 @Override 1885 public void onAnimationEnd(Animator animation) { 1886 // Make sure animation ends up having the correct scale even 1887 // if it is cancelled before it finishes 1888 if (mScale != endScale) { 1889 current.postScale(focusX, focusY, endScale / mScale, mDrawArea.width(), 1890 mDrawArea.height()); 1891 mScale = endScale; 1892 } 1893 1894 if (mScale == FULL_SCREEN_SCALE) { 1895 setSurroundingViewsVisible(true); 1896 mZoomView.setVisibility(GONE); 1897 current.resetTransform(); 1898 onEnterFullScreen(); 1899 } else { 1900 mController.loadZoomedImage(); 1901 onEnterZoomView(); 1902 } 1903 mZoomAnimator = null; 1904 } 1905 1906 @Override 1907 public void onAnimationCancel(Animator animation) { 1908 // Do nothing. 1909 } 1910 1911 @Override 1912 public void onAnimationRepeat(Animator animation) { 1913 // Do nothing. 1914 } 1915 }); 1916 1917 mZoomAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1918 @Override 1919 public void onAnimationUpdate(ValueAnimator animation) { 1920 float newScale = (Float) animation.getAnimatedValue(); 1921 float postScale = newScale / mScale; 1922 mScale = newScale; 1923 current.postScale(focusX, focusY, postScale, mDrawArea.width(), 1924 mDrawArea.height()); 1925 } 1926 }); 1927 mZoomAnimator.start(); 1928 } 1929 1930 @Override 1931 public void scroll(float deltaX) { 1932 if (!stopScrolling(false)) { 1933 return; 1934 } 1935 mCenterX += deltaX; 1936 1937 boolean stopScroll = clampCenterX(); 1938 if (stopScroll) { 1939 mController.stopScrolling(true); 1940 } 1941 invalidate(); 1942 } 1943 1944 @Override 1945 public void fling(float velocityX) { 1946 if (!stopScrolling(false)) { 1947 return; 1948 } 1949 final ViewItem item = mViewItem[mCurrentItem]; 1950 if (item == null) { 1951 return; 1952 } 1953 1954 float scaledVelocityX = velocityX / mScale; 1955 if (inFullScreen() && isViewTypeSticky(item) && scaledVelocityX < 0) { 1956 // Swipe left in camera preview. 1957 goToFilmstrip(); 1958 } 1959 1960 int w = getWidth(); 1961 // Estimation of possible length on the left. To ensure the 1962 // velocity doesn't become too slow eventually, we add a huge number 1963 // to the estimated maximum. 1964 int minX = estimateMinX(item.getId(), item.getLeftPosition(), w); 1965 // Estimation of possible length on the right. Likewise, exaggerate 1966 // the possible maximum too. 1967 int maxX = estimateMaxX(item.getId(), item.getLeftPosition(), w); 1968 mScroller.fling(mCenterX, 0, (int) -velocityX, 0, minX, maxX, 0, 0); 1969 } 1970 1971 void flingInsideZoomView(float velocityX, float velocityY) { 1972 if (!isZoomStarted()) { 1973 return; 1974 } 1975 1976 final ViewItem current = mViewItem[mCurrentItem]; 1977 if (current == null) { 1978 return; 1979 } 1980 1981 final int factor = DECELERATION_FACTOR; 1982 // Deceleration curve for distance: 1983 // S(t) = s + (e - s) * (1 - (1 - t/T) ^ factor) 1984 // Need to find the ending distance (e), so that the starting 1985 // velocity is the velocity of fling. 1986 // Velocity is the derivative of distance 1987 // V(t) = (e - s) * factor * (-1) * (1 - t/T) ^ (factor - 1) * (-1/T) 1988 // = (e - s) * factor * (1 - t/T) ^ (factor - 1) / T 1989 // Since V(0) = V0, we have e = T / factor * V0 + s 1990 1991 // Duration T should be long enough so that at the end of the fling, 1992 // image moves at 1 pixel/s for about P = 50ms = 0.05s 1993 // i.e. V(T - P) = 1 1994 // V(T - P) = V0 * (1 - (T -P) /T) ^ (factor - 1) = 1 1995 // T = P * V0 ^ (1 / (factor -1)) 1996 1997 final float velocity = Math.max(Math.abs(velocityX), Math.abs(velocityY)); 1998 // Dynamically calculate duration 1999 final float duration = (float) (FLING_COASTING_DURATION_S 2000 * Math.pow(velocity, (1f / (factor - 1f)))); 2001 2002 final float translationX = current.getTranslationX() * mScale; 2003 final float translationY = current.getTranslationY() * mScale; 2004 2005 final ValueAnimator decelerationX = ValueAnimator.ofFloat(translationX, 2006 translationX + duration / factor * velocityX); 2007 final ValueAnimator decelerationY = ValueAnimator.ofFloat(translationY, 2008 translationY + duration / factor * velocityY); 2009 2010 decelerationY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 2011 @Override 2012 public void onAnimationUpdate(ValueAnimator animation) { 2013 float transX = (Float) decelerationX.getAnimatedValue(); 2014 float transY = (Float) decelerationY.getAnimatedValue(); 2015 2016 current.updateTransform(transX, transY, mScale, 2017 mScale, mDrawArea.width(), mDrawArea.height()); 2018 } 2019 }); 2020 2021 mFlingAnimator = new AnimatorSet(); 2022 mFlingAnimator.play(decelerationX).with(decelerationY); 2023 mFlingAnimator.setDuration((int) (duration * 1000)); 2024 mFlingAnimator.setInterpolator(new TimeInterpolator() { 2025 @Override 2026 public float getInterpolation(float input) { 2027 return (float) (1.0f - Math.pow((1.0f - input), factor)); 2028 } 2029 }); 2030 mFlingAnimator.addListener(new Animator.AnimatorListener() { 2031 private boolean mCancelled = false; 2032 2033 @Override 2034 public void onAnimationStart(Animator animation) { 2035 2036 } 2037 2038 @Override 2039 public void onAnimationEnd(Animator animation) { 2040 if (!mCancelled) { 2041 loadZoomedImage(); 2042 } 2043 mFlingAnimator = null; 2044 } 2045 2046 @Override 2047 public void onAnimationCancel(Animator animation) { 2048 mCancelled = true; 2049 } 2050 2051 @Override 2052 public void onAnimationRepeat(Animator animation) { 2053 2054 } 2055 }); 2056 mFlingAnimator.start(); 2057 } 2058 2059 @Override 2060 public boolean stopScrolling(boolean forced) { 2061 if (!isScrolling()) { 2062 return true; 2063 } else if (!mCanStopScroll && !forced) { 2064 return false; 2065 } 2066 mScroller.forceFinished(true); 2067 return true; 2068 } 2069 2070 private void stopScale() { 2071 mScaleAnimator.cancel(); 2072 } 2073 2074 @Override 2075 public void scrollToPosition(int position, int duration, boolean interruptible) { 2076 if (mViewItem[mCurrentItem] == null) { 2077 return; 2078 } 2079 mCanStopScroll = interruptible; 2080 mScroller.startScroll(mCenterX, 0, position - mCenterX, 0, duration); 2081 } 2082 2083 @Override 2084 public boolean goToNextItem() { 2085 final ViewItem nextItem = mViewItem[mCurrentItem + 1]; 2086 if (nextItem == null) { 2087 return false; 2088 } 2089 stopScrolling(true); 2090 scrollToPosition(nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS * 2, false); 2091 2092 if (isViewTypeSticky(mViewItem[mCurrentItem])) { 2093 // Special case when moving from camera preview. 2094 scaleTo(FILM_STRIP_SCALE, GEOMETRY_ADJUST_TIME_MS); 2095 } 2096 return true; 2097 } 2098 2099 private void scaleTo(float scale, int duration) { 2100 if (mViewItem[mCurrentItem] == null) { 2101 return; 2102 } 2103 stopScale(); 2104 mScaleAnimator.setDuration(duration); 2105 mScaleAnimator.setFloatValues(mScale, scale); 2106 mScaleAnimator.start(); 2107 } 2108 2109 @Override 2110 public void goToFilmstrip() { 2111 if (mViewItem[mCurrentItem] == null) { 2112 return; 2113 } 2114 if (mScale == FILM_STRIP_SCALE) { 2115 return; 2116 } 2117 scaleTo(FILM_STRIP_SCALE, GEOMETRY_ADJUST_TIME_MS); 2118 2119 final ViewItem currItem = mViewItem[mCurrentItem]; 2120 final ViewItem nextItem = mViewItem[mCurrentItem + 1]; 2121 if (currItem.getId() == 0 && isViewTypeSticky(currItem) && nextItem != null) { 2122 // Deal with the special case of swiping in camera preview. 2123 scrollToPosition(nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, false); 2124 } 2125 2126 if (mScale == FILM_STRIP_SCALE) { 2127 onLeaveFilmstrip(); 2128 } 2129 } 2130 2131 @Override 2132 public void goToFullScreen() { 2133 if (inFullScreen()) { 2134 return; 2135 } 2136 scaleTo(FULL_SCREEN_SCALE, GEOMETRY_ADJUST_TIME_MS); 2137 if (mScale == FULL_SCREEN_SCALE) { 2138 onLeaveFullScreen(); 2139 } 2140 } 2141 2142 private void cancelFlingAnimation() { 2143 // Cancels flinging for zoomed images 2144 if (isFlingAnimationRunning()) { 2145 mFlingAnimator.cancel(); 2146 } 2147 } 2148 2149 private void cancelZoomAnimation() { 2150 if (isZoomAnimationRunning()) { 2151 mZoomAnimator.cancel(); 2152 } 2153 } 2154 2155 private void setSurroundingViewsVisible(boolean visible) { 2156 // Hide everything on the left 2157 // TODO: Need to find a better way to toggle the visibility of views 2158 // around the current view. 2159 for (int i = 0; i < mCurrentItem; i++) { 2160 if (i == mCurrentItem || mViewItem[i] == null) { 2161 continue; 2162 } 2163 mViewItem[i].setVisibility(visible ? VISIBLE : INVISIBLE); 2164 } 2165 } 2166 2167 private Uri getCurrentContentUri() { 2168 ViewItem curr = mViewItem[mCurrentItem]; 2169 if (curr == null) { 2170 return Uri.EMPTY; 2171 } 2172 return mDataAdapter.getImageData(curr.getId()).getContentUri(); 2173 } 2174 2175 /** 2176 * Here we only support up to 1:1 image zoom (i.e. a 100% view of the 2177 * actual pixels). The max scale that we can apply on the view should 2178 * make the view same size as the image, in pixels. 2179 */ 2180 private float getCurrentDataMaxScale(boolean allowOverScale) { 2181 ViewItem curr = mViewItem[mCurrentItem]; 2182 ImageData imageData = mDataAdapter.getImageData(curr.getId()); 2183 if (curr == null || !imageData 2184 .isUIActionSupported(ImageData.ACTION_ZOOM)) { 2185 return FULL_SCREEN_SCALE; 2186 } 2187 float imageWidth = imageData.getWidth(); 2188 if (imageData.getOrientation() == 90 2189 || imageData.getOrientation() == 270) { 2190 imageWidth = imageData.getHeight(); 2191 } 2192 float scale = imageWidth / curr.getWidth(); 2193 if (allowOverScale) { 2194 // In addition to the scale we apply to the view for 100% view 2195 // (i.e. each pixel on screen corresponds to a pixel in image) 2196 // we allow scaling beyond that for better detail viewing. 2197 scale *= mOverScaleFactor; 2198 } 2199 return scale; 2200 } 2201 2202 private void loadZoomedImage() { 2203 if (!isZoomStarted()) { 2204 return; 2205 } 2206 ViewItem curr = mViewItem[mCurrentItem]; 2207 if (curr == null) { 2208 return; 2209 } 2210 ImageData imageData = mDataAdapter.getImageData(curr.getId()); 2211 if (!imageData.isUIActionSupported(ImageData.ACTION_ZOOM)) { 2212 return; 2213 } 2214 Uri uri = getCurrentContentUri(); 2215 RectF viewRect = curr.getViewRect(); 2216 if (uri == null || uri == Uri.EMPTY) { 2217 return; 2218 } 2219 int orientation = imageData.getOrientation(); 2220 mZoomView.loadBitmap(uri, orientation, viewRect); 2221 } 2222 2223 private void cancelLoadingZoomedImage() { 2224 mZoomView.cancelPartialDecodingTask(); 2225 } 2226 2227 @Override 2228 public void goToFirstItem() { 2229 if (mViewItem[mCurrentItem] == null) { 2230 return; 2231 } 2232 resetZoomView(); 2233 // TODO: animate to camera if it is still in the mViewItem buffer 2234 // versus a full reload which will perform an immediate transition 2235 reload(); 2236 } 2237 2238 public boolean isZoomStarted() { 2239 return mScale > FULL_SCREEN_SCALE; 2240 } 2241 2242 public boolean isFlingAnimationRunning() { 2243 return mFlingAnimator != null && mFlingAnimator.isRunning(); 2244 } 2245 2246 public boolean isZoomAnimationRunning() { 2247 return mZoomAnimator != null && mZoomAnimator.isRunning(); 2248 } 2249 } 2250 2251 private boolean isCurrentItemCentered() { 2252 return mViewItem[mCurrentItem].getCenterX() == mCenterX; 2253 } 2254 2255 private static class MyScroller { 2256 public interface Listener { 2257 public void onScrollUpdate(int currX, int currY); 2258 2259 public void onScrollEnd(); 2260 } 2261 2262 private final Handler mHandler; 2263 private final Listener mListener; 2264 2265 private final Scroller mScroller; 2266 2267 private final ValueAnimator mXScrollAnimator; 2268 private final Runnable mScrollChecker = new Runnable() { 2269 @Override 2270 public void run() { 2271 boolean newPosition = mScroller.computeScrollOffset(); 2272 if (!newPosition) { 2273 mListener.onScrollEnd(); 2274 return; 2275 } 2276 mListener.onScrollUpdate(mScroller.getCurrX(), mScroller.getCurrY()); 2277 mHandler.removeCallbacks(this); 2278 mHandler.post(this); 2279 } 2280 }; 2281 2282 private final ValueAnimator.AnimatorUpdateListener mXScrollAnimatorUpdateListener = 2283 new ValueAnimator.AnimatorUpdateListener() { 2284 @Override 2285 public void onAnimationUpdate(ValueAnimator animation) { 2286 mListener.onScrollUpdate((Integer) animation.getAnimatedValue(), 0); 2287 } 2288 }; 2289 2290 private final Animator.AnimatorListener mXScrollAnimatorListener = 2291 new Animator.AnimatorListener() { 2292 @Override 2293 public void onAnimationCancel(Animator animation) { 2294 // Do nothing. 2295 } 2296 2297 @Override 2298 public void onAnimationEnd(Animator animation) { 2299 mListener.onScrollEnd(); 2300 } 2301 2302 @Override 2303 public void onAnimationRepeat(Animator animation) { 2304 // Do nothing. 2305 } 2306 2307 @Override 2308 public void onAnimationStart(Animator animation) { 2309 // Do nothing. 2310 } 2311 }; 2312 2313 public MyScroller(Context ctx, Handler handler, Listener listener, 2314 TimeInterpolator interpolator) { 2315 mHandler = handler; 2316 mListener = listener; 2317 mScroller = new Scroller(ctx); 2318 mXScrollAnimator = new ValueAnimator(); 2319 mXScrollAnimator.addUpdateListener(mXScrollAnimatorUpdateListener); 2320 mXScrollAnimator.addListener(mXScrollAnimatorListener); 2321 mXScrollAnimator.setInterpolator(interpolator); 2322 } 2323 2324 public void fling( 2325 int startX, int startY, 2326 int velocityX, int velocityY, 2327 int minX, int maxX, 2328 int minY, int maxY) { 2329 mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY); 2330 runChecker(); 2331 } 2332 2333 public void startScroll(int startX, int startY, int dx, int dy) { 2334 mScroller.startScroll(startX, startY, dx, dy); 2335 runChecker(); 2336 } 2337 2338 /** Only starts and updates scroll in x-axis. */ 2339 public void startScroll(int startX, int startY, int dx, int dy, int duration) { 2340 mXScrollAnimator.cancel(); 2341 mXScrollAnimator.setDuration(duration); 2342 mXScrollAnimator.setIntValues(startX, startX + dx); 2343 mXScrollAnimator.start(); 2344 } 2345 2346 public boolean isFinished() { 2347 return (mScroller.isFinished() && !mXScrollAnimator.isRunning()); 2348 } 2349 2350 public void forceFinished(boolean finished) { 2351 mScroller.forceFinished(finished); 2352 if (finished) { 2353 mXScrollAnimator.cancel(); 2354 } 2355 } 2356 2357 private void runChecker() { 2358 if (mHandler == null || mListener == null) { 2359 return; 2360 } 2361 mHandler.removeCallbacks(mScrollChecker); 2362 mHandler.post(mScrollChecker); 2363 } 2364 } 2365 2366 private class MyGestureReceiver implements FilmstripGestureRecognizer.Listener { 2367 2368 private static final int SCROLL_DIR_NONE = 0; 2369 private static final int SCROLL_DIR_VERTICAL = 1; 2370 private static final int SCROLL_DIR_HORIZONTAL = 2; 2371 // Indicating the current trend of scaling is up (>1) or down (<1). 2372 private float mScaleTrend; 2373 private float mMaxScale; 2374 private int mScrollingDirection = SCROLL_DIR_NONE; 2375 2376 2377 @Override 2378 public boolean onSingleTapUp(float x, float y) { 2379 ViewItem centerItem = mViewItem[mCurrentItem]; 2380 if (inFilmstrip()) { 2381 if (centerItem != null && centerItem.areaContains(x, y)) { 2382 mController.goToFullScreen(); 2383 return true; 2384 } 2385 } else if (inFullScreen()) { 2386 mController.goToFilmstrip(); 2387 return true; 2388 } 2389 return false; 2390 } 2391 2392 @Override 2393 public boolean onDoubleTap(float x, float y) { 2394 ViewItem current = mViewItem[mCurrentItem]; 2395 if (inFilmstrip() && current != null) { 2396 mController.goToFullScreen(); 2397 return true; 2398 } else if (mScale < FULL_SCREEN_SCALE || inCameraFullscreen()) { 2399 return false; 2400 } 2401 if (current == null) { 2402 return false; 2403 } 2404 if (!mController.stopScrolling(false)) { 2405 return false; 2406 } 2407 mController.zoomAt(current, x, y); 2408 checkItemAtMaxSize(); 2409 return true; 2410 } 2411 2412 @Override 2413 public boolean onDown(float x, float y) { 2414 mController.cancelFlingAnimation(); 2415 if (!mController.stopScrolling(false)) { 2416 return false; 2417 } 2418 2419 return true; 2420 } 2421 2422 @Override 2423 public boolean onUp(float x, float y) { 2424 ViewItem currItem = mViewItem[mCurrentItem]; 2425 if (currItem == null) { 2426 return false; 2427 } 2428 if (mController.isZoomAnimationRunning() || mController.isFlingAnimationRunning()) { 2429 return false; 2430 } 2431 if (mController.isZoomStarted()) { 2432 mController.loadZoomedImage(); 2433 return true; 2434 } 2435 float halfH = getHeight() / 2; 2436 mIsUserScrolling = false; 2437 mScrollingDirection = SCROLL_DIR_NONE; 2438 // Finds items promoted/demoted. 2439 for (int i = 0; i < BUFFER_SIZE; i++) { 2440 if (mViewItem[i] == null) { 2441 continue; 2442 } 2443 float transY = mViewItem[i].getTranslationY(); 2444 if (transY == 0) { 2445 continue; 2446 } 2447 int id = mViewItem[i].getId(); 2448 2449 if (mDataAdapter.getImageData(id) 2450 .isUIActionSupported(ImageData.ACTION_DEMOTE) 2451 && transY > halfH) { 2452 demoteData(i, id); 2453 } else if (mDataAdapter.getImageData(id) 2454 .isUIActionSupported(ImageData.ACTION_PROMOTE) 2455 && transY < -halfH) { 2456 promoteData(i, id); 2457 } else { 2458 // put the view back. 2459 slideViewBack(mViewItem[i]); 2460 } 2461 } 2462 2463 // The data might be changed. Re-check. 2464 currItem = mViewItem[mCurrentItem]; 2465 if (currItem == null) { 2466 return true; 2467 } 2468 2469 int currId = currItem.getId(); 2470 if (mCenterX > currItem.getCenterX() + CAMERA_PREVIEW_SWIPE_THRESHOLD && currId == 0 && 2471 isViewTypeSticky(currItem) && mDataIdOnUserScrolling == 0) { 2472 mController.goToFilmstrip(); 2473 // Special case to go from camera preview to the next photo. 2474 if (mViewItem[mCurrentItem + 1] != null) { 2475 mController.scrollToPosition( 2476 mViewItem[mCurrentItem + 1].getCenterX(), 2477 GEOMETRY_ADJUST_TIME_MS, false); 2478 } else { 2479 // No next photo. 2480 snapInCenter(); 2481 } 2482 } 2483 if (isCurrentItemCentered() && currId == 0 && isViewTypeSticky(currItem)) { 2484 mController.goToFullScreen(); 2485 } else { 2486 if (mDataIdOnUserScrolling == 0 && currId != 0) { 2487 // Special case to go to filmstrip when the user scroll away 2488 // from the camera preview and the current one is not the 2489 // preview anymore. 2490 mController.goToFilmstrip(); 2491 mDataIdOnUserScrolling = currId; 2492 } 2493 snapInCenter(); 2494 } 2495 return false; 2496 } 2497 2498 @Override 2499 public boolean onScroll(float x, float y, float dx, float dy) { 2500 final ViewItem currItem = mViewItem[mCurrentItem]; 2501 if (currItem == null) { 2502 return false; 2503 } 2504 if (!mDataAdapter.canSwipeInFullScreen(currItem.getId())) { 2505 return false; 2506 } 2507 hideZoomView(); 2508 // When image is zoomed in to be bigger than the screen 2509 if (mController.isZoomStarted()) { 2510 ViewItem curr = mViewItem[mCurrentItem]; 2511 float transX = curr.getTranslationX() * mScale - dx; 2512 float transY = curr.getTranslationY() * mScale - dy; 2513 curr.updateTransform(transX, transY, mScale, mScale, mDrawArea.width(), 2514 mDrawArea.height()); 2515 return true; 2516 } 2517 int deltaX = (int) (dx / mScale); 2518 // Forces the current scrolling to stop. 2519 mController.stopScrolling(true); 2520 if (!mIsUserScrolling) { 2521 mIsUserScrolling = true; 2522 mDataIdOnUserScrolling = mViewItem[mCurrentItem].getId(); 2523 } 2524 if (inFilmstrip()) { 2525 // Disambiguate horizontal/vertical first. 2526 if (mScrollingDirection == SCROLL_DIR_NONE) { 2527 mScrollingDirection = (Math.abs(dx) > Math.abs(dy)) ? SCROLL_DIR_HORIZONTAL : 2528 SCROLL_DIR_VERTICAL; 2529 } 2530 if (mScrollingDirection == SCROLL_DIR_HORIZONTAL) { 2531 if (mCenterX == currItem.getCenterX() && currItem.getId() == 0 && dx < 0) { 2532 // Already at the beginning, don't process the swipe. 2533 mIsUserScrolling = false; 2534 mScrollingDirection = SCROLL_DIR_NONE; 2535 return false; 2536 } 2537 mController.scroll(deltaX); 2538 } else { 2539 // Vertical part. Promote or demote. 2540 int hit = 0; 2541 Rect hitRect = new Rect(); 2542 for (; hit < BUFFER_SIZE; hit++) { 2543 if (mViewItem[hit] == null) { 2544 continue; 2545 } 2546 mViewItem[hit].getHitRect(hitRect); 2547 if (hitRect.contains((int) x, (int) y)) { 2548 break; 2549 } 2550 } 2551 if (hit == BUFFER_SIZE) { 2552 // Hit none. 2553 return true; 2554 } 2555 2556 ImageData data = mDataAdapter.getImageData(mViewItem[hit].getId()); 2557 float transY = mViewItem[hit].getTranslationY() - dy / mScale; 2558 if (!data.isUIActionSupported(ImageData.ACTION_DEMOTE) && 2559 transY > 0f) { 2560 transY = 0f; 2561 } 2562 if (!data.isUIActionSupported(ImageData.ACTION_PROMOTE) && 2563 transY < 0f) { 2564 transY = 0f; 2565 } 2566 mViewItem[hit].setTranslationY(transY); 2567 } 2568 } else if (inFullScreen()) { 2569 if (mViewItem[mCurrentItem] == null || (deltaX < 0 && mCenterX <= 2570 currItem.getCenterX() && currItem.getId() == 0)) { 2571 return false; 2572 } 2573 // Multiplied by 1.2 to make it more easy to swipe. 2574 mController.scroll((int) (deltaX * 1.2)); 2575 } 2576 invalidate(); 2577 2578 return true; 2579 } 2580 2581 @Override 2582 public boolean onFling(float velocityX, float velocityY) { 2583 final ViewItem currItem = mViewItem[mCurrentItem]; 2584 if (currItem == null) { 2585 return false; 2586 } 2587 if (!mDataAdapter.canSwipeInFullScreen(currItem.getId())) { 2588 return false; 2589 } 2590 if (mController.isZoomStarted()) { 2591 // Fling within the zoomed image 2592 mController.flingInsideZoomView(velocityX, velocityY); 2593 return true; 2594 } 2595 if (Math.abs(velocityX) < Math.abs(velocityY)) { 2596 // ignore vertical fling. 2597 return true; 2598 } 2599 2600 // In full-screen, fling of a velocity above a threshold should go 2601 // to the next/prev photos 2602 if (mScale == FULL_SCREEN_SCALE) { 2603 int currItemCenterX = currItem.getCenterX(); 2604 2605 if (velocityX > 0) { // left 2606 if (mCenterX > currItemCenterX) { 2607 // The visually previous item is actually the current 2608 // item. 2609 mController.scrollToPosition( 2610 currItemCenterX, GEOMETRY_ADJUST_TIME_MS, true); 2611 return true; 2612 } 2613 ViewItem prevItem = mViewItem[mCurrentItem - 1]; 2614 if (prevItem == null) { 2615 return false; 2616 } 2617 mController.scrollToPosition( 2618 prevItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, true); 2619 } else { // right 2620 if (mController.stopScrolling(false)) { 2621 if (mCenterX < currItemCenterX) { 2622 // The visually next item is actually the current 2623 // item. 2624 mController.scrollToPosition( 2625 currItemCenterX, GEOMETRY_ADJUST_TIME_MS, true); 2626 return true; 2627 } 2628 final ViewItem nextItem = mViewItem[mCurrentItem + 1]; 2629 if (nextItem == null) { 2630 return false; 2631 } 2632 mController.scrollToPosition( 2633 nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, true); 2634 if (isViewTypeSticky(currItem)) { 2635 mController.goToFilmstrip(); 2636 } 2637 } 2638 } 2639 } 2640 2641 if (mScale == FILM_STRIP_SCALE) { 2642 mController.fling(velocityX); 2643 } 2644 return true; 2645 } 2646 2647 @Override 2648 public boolean onScaleBegin(float focusX, float focusY) { 2649 if (inCameraFullscreen()) { 2650 return false; 2651 } 2652 2653 hideZoomView(); 2654 mScaleTrend = 1f; 2655 // If the image is smaller than screen size, we should allow to zoom 2656 // in to full screen size 2657 mMaxScale = Math.max(mController.getCurrentDataMaxScale(true), FULL_SCREEN_SCALE); 2658 return true; 2659 } 2660 2661 @Override 2662 public boolean onScale(float focusX, float focusY, float scale) { 2663 if (inCameraFullscreen()) { 2664 return false; 2665 } 2666 2667 mScaleTrend = mScaleTrend * 0.3f + scale * 0.7f; 2668 float newScale = mScale * scale; 2669 if (mScale < FULL_SCREEN_SCALE && newScale < FULL_SCREEN_SCALE) { 2670 if (newScale <= FILM_STRIP_SCALE) { 2671 newScale = FILM_STRIP_SCALE; 2672 } 2673 // Scaled view is smaller than or equal to screen size both 2674 // before and after scaling 2675 if (mScale != newScale) { 2676 if (mScale == FILM_STRIP_SCALE) { 2677 onLeaveFilmstrip(); 2678 } 2679 if (newScale == FILM_STRIP_SCALE) { 2680 onEnterFilmstrip(); 2681 } 2682 } 2683 mScale = newScale; 2684 invalidate(); 2685 } else if (mScale < FULL_SCREEN_SCALE && newScale >= FULL_SCREEN_SCALE) { 2686 // Going from smaller than screen size to bigger than or equal 2687 // to screen size 2688 if (mScale == FILM_STRIP_SCALE) { 2689 onLeaveFilmstrip(); 2690 } 2691 mScale = FULL_SCREEN_SCALE; 2692 onEnterFullScreen(); 2693 invalidate(); 2694 mController.setSurroundingViewsVisible(false); 2695 } else if (mScale >= FULL_SCREEN_SCALE && newScale < FULL_SCREEN_SCALE) { 2696 // Going from bigger than or equal to screen size to smaller 2697 // than screen size 2698 if (mScale == FULL_SCREEN_SCALE) { 2699 onLeaveFullScreen(); 2700 } 2701 mScale = newScale; 2702 invalidate(); 2703 mController.setSurroundingViewsVisible(true); 2704 } else { 2705 // Scaled view bigger than or equal to screen size both before 2706 // and after scaling 2707 if (!mController.isZoomStarted()) { 2708 mController.setSurroundingViewsVisible(false); 2709 } 2710 ViewItem curr = mViewItem[mCurrentItem]; 2711 // Make sure the image is not overly scaled 2712 newScale = Math.min(newScale, mMaxScale); 2713 if (newScale == mScale) { 2714 return true; 2715 } 2716 float postScale = newScale / mScale; 2717 curr.postScale(focusX, focusY, postScale, mDrawArea.width(), mDrawArea.height()); 2718 mScale = newScale; 2719 if (mScale == FULL_SCREEN_SCALE) { 2720 onEnterFullScreen(); 2721 } 2722 checkItemAtMaxSize(); 2723 } 2724 return true; 2725 } 2726 2727 @Override 2728 public void onScaleEnd() { 2729 if (mScale > FULL_SCREEN_SCALE + TOLERANCE) { 2730 return; 2731 } 2732 mController.setSurroundingViewsVisible(true); 2733 if (mScale <= FILM_STRIP_SCALE + TOLERANCE) { 2734 mController.goToFilmstrip(); 2735 } else if (mScaleTrend > 1f || mScale > FULL_SCREEN_SCALE - TOLERANCE) { 2736 if (mController.isZoomStarted()) { 2737 mScale = FULL_SCREEN_SCALE; 2738 resetZoomView(); 2739 } 2740 mController.goToFullScreen(); 2741 } else { 2742 mController.goToFilmstrip(); 2743 } 2744 mScaleTrend = 1f; 2745 } 2746 } 2747} 2748