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