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