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