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