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