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