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